From 553f2d60f7d507edffd223337976f5c0479065c9 Mon Sep 17 00:00:00 2001 From: Owczarek Kamil Date: Sat, 22 Jun 2024 16:19:04 +0200 Subject: [PATCH] created basic recomentation algorithm --- Database/init-db.sql | 12 ++ .../ReasnAPI/Controllers/UsersController.cs | 36 +++++- .../ReasnAPI/Mappers/UserInterestMapper.cs | 5 +- .../ReasnAPI/Services/EventService.cs | 1 - .../ReasnAPI/Services/RecomendationService.cs | 62 ++++++++++ .../ReasnAPI/ReasnAPI/Services/UserService.cs | 108 +++++++++++----- realeated.py | 117 ++++++++++++++++++ recomendation.py | 58 +++++++++ translated.py | 74 +++++++++++ 9 files changed, 440 insertions(+), 33 deletions(-) create mode 100644 Server/ReasnAPI/ReasnAPI/Services/RecomendationService.cs create mode 100644 realeated.py create mode 100644 recomendation.py create mode 100644 translated.py diff --git a/Database/init-db.sql b/Database/init-db.sql index e178ba36..c15522c2 100644 --- a/Database/init-db.sql +++ b/Database/init-db.sql @@ -104,6 +104,18 @@ CREATE TABLE IF NOT EXISTS users.interest ( "name" text NOT NULL CONSTRAINT users_interest_name_maxlength CHECK (LENGTH("name") <= 32) UNIQUE ); +CREATE TABLE IF NOT EXISTS common.related ( + "tag_name" text, + "interest_name" text, + "value" decimal, + UNIQUE ("tag_name", "interest_name") +); + +CREATE TABLE IF NOT EXISTS common.translated ( + "name_pl" text, + "name_ang" text +); + ALTER TABLE events.event ADD FOREIGN KEY ("address_id") REFERENCES common.address ("id"); ALTER TABLE events.comment ADD FOREIGN KEY ("event_id") REFERENCES events.event ("id"); diff --git a/Server/ReasnAPI/ReasnAPI/Controllers/UsersController.cs b/Server/ReasnAPI/ReasnAPI/Controllers/UsersController.cs index ca021725..dc9c269b 100644 --- a/Server/ReasnAPI/ReasnAPI/Controllers/UsersController.cs +++ b/Server/ReasnAPI/ReasnAPI/Controllers/UsersController.cs @@ -1,4 +1,13 @@ using Microsoft.AspNetCore.Mvc; +using ReasnAPI.Services; +using System.Net.Http; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using ReasnAPI.Models.Database; + namespace ReasnAPI.Controllers; @@ -6,10 +15,35 @@ namespace ReasnAPI.Controllers; [Route("[controller]")] public class UsersController : ControllerBase { + private readonly RecomendationService _recomendationService; + private readonly UserService _userService; + private readonly HttpClient _httpClient; + + public UsersController(RecomendationService recomendationService, UserService userService, HttpClient httpClient) + { + _recomendationService = recomendationService; + _userService = userService; + _httpClient = httpClient; + } + [HttpGet] [Route("{username}")] public IActionResult GetUserByUsername(string username) { - throw new NotImplementedException(); + var user =_userService.GetUserByUsername(username); + return Ok(user); + } + + [HttpGet] + [Route("{username}/recomendetevents")] + public async Task GetRecomendetEvents(string username) + { + var currentUser = _userService.GetUserByUsername(username); + var interests = currentUser.Interests; + + // Poprawka: Dodanie await i poprawienie błędu w nazwie metody + var events = await _recomendationService.GetEventsByInterest(interests); + + return Ok(events); } } \ No newline at end of file diff --git a/Server/ReasnAPI/ReasnAPI/Mappers/UserInterestMapper.cs b/Server/ReasnAPI/ReasnAPI/Mappers/UserInterestMapper.cs index 19ac06bb..42c9325b 100644 --- a/Server/ReasnAPI/ReasnAPI/Mappers/UserInterestMapper.cs +++ b/Server/ReasnAPI/ReasnAPI/Mappers/UserInterestMapper.cs @@ -19,11 +19,12 @@ public static List ToDtoList(this IEnumerable use return userInterests.Select(ToDto).ToList(); } - public static UserInterest ToEntity(this UserInterestDto userInterestDto) + public static UserInterest ToEntity(this UserInterestDto userInterestDto, int userId, int interestId) { return new UserInterest { - Interest = userInterestDto.Interest.ToEntity(), + UserId = userId, + InterestId = interestId, Level = userInterestDto.Level }; } diff --git a/Server/ReasnAPI/ReasnAPI/Services/EventService.cs b/Server/ReasnAPI/ReasnAPI/Services/EventService.cs index 3e3d3709..b65d99f8 100644 --- a/Server/ReasnAPI/ReasnAPI/Services/EventService.cs +++ b/Server/ReasnAPI/ReasnAPI/Services/EventService.cs @@ -11,7 +11,6 @@ namespace ReasnAPI.Services; public class EventService(ReasnContext context, ParameterService parameterService, TagService tagService, CommentService commentService) { - public EventDto CreateEvent(EventDto eventDto) { using (var scope = new TransactionScope()) diff --git a/Server/ReasnAPI/ReasnAPI/Services/RecomendationService.cs b/Server/ReasnAPI/ReasnAPI/Services/RecomendationService.cs new file mode 100644 index 00000000..0ddbf0ed --- /dev/null +++ b/Server/ReasnAPI/ReasnAPI/Services/RecomendationService.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using ReasnAPI.Models.Database; +using ReasnAPI.Models.DTOs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using ReasnAPI.Mappers; + +namespace ReasnAPI.Services +{ + public class RecomendationService + { + private readonly HttpClient httpClient; + private readonly ReasnContext context; + private readonly string flaskApiUrl; + + public RecomendationService(HttpClient httpClient, ReasnContext context, IConfiguration configuration) + { + this.httpClient = httpClient; + this.context = context; + this.flaskApiUrl = $"{configuration.GetValue("FlaskApi:BaseUrl")}/similar-tags"; + } + + public async Task> GetEventsByInterest(List interestsDto) + { + var interests = interestsDto.Select(i => i.Interest.Name).ToList(); + + try + { + var response = await httpClient.PostAsJsonAsync(flaskApiUrl, interests); + + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException( + $"Error fetching tags from Flask API. Status code: {response.StatusCode}"); + } + + var tagNames = await response.Content.ReadFromJsonAsync>(); + + if (tagNames == null || tagNames.Count == 0) + { + return new List(); + } + + var events = await context.Events + .Include(e => e.Tags).Include(e => e.Parameters) + .Where(e => e.Tags.Any(t => tagNames.Contains(t.Name))) + .ToListAsync(); + + return events.ToDtoList(); + } + catch (Exception ex) + { + Console.WriteLine($"Exception occurred while fetching events: {ex.Message}"); + throw; + } + } + } +} diff --git a/Server/ReasnAPI/ReasnAPI/Services/UserService.cs b/Server/ReasnAPI/ReasnAPI/Services/UserService.cs index 92a187a7..007c56ab 100644 --- a/Server/ReasnAPI/ReasnAPI/Services/UserService.cs +++ b/Server/ReasnAPI/ReasnAPI/Services/UserService.cs @@ -3,81 +3,115 @@ using ReasnAPI.Mappers; using ReasnAPI.Models.Database; using ReasnAPI.Models.DTOs; +using Serilog; using System.Linq.Expressions; +using System.Security.Claims; using System.Transactions; namespace ReasnAPI.Services; -public class UserService(ReasnContext context) +public class UserService { - public UserDto UpdateUser(int userId, UserDto userDto) + private readonly ReasnContext _context; + private readonly IHttpContextAccessor _httpContextAccessor; + + public UserService(ReasnContext context) + { + _context = context; + } + + public UserService(ReasnContext context, IHttpContextAccessor httpContextAccessor) + { + _context = context; + _httpContextAccessor = httpContextAccessor; + } + + public User GetCurrentUser() + { + var httpContext = _httpContextAccessor.HttpContext; + + if (httpContext is null) + { + throw new InvalidOperationException("No HTTP context available"); + } + + var email = httpContext.User.FindFirstValue(ClaimTypes.Email); + if (string.IsNullOrEmpty(email)) + { + throw new UnauthorizedAccessException("No email claim found in token"); + } + + var user = _context.Users.FirstOrDefault(u => u.Email == email); + + if (user is null) + { + throw new NotFoundException("User associated with email not found"); + } + + return user; + } + + public UserDto UpdateUser(string username, UserDto userDto) { using (var scope = new TransactionScope()) { ArgumentNullException.ThrowIfNull(userDto); - var user = context.Users + var user = _context.Users .Include(u => u.UserInterests) .ThenInclude(ui => ui.Interest) - .FirstOrDefault(r => r.Id == userId); + .FirstOrDefault(r => r.Username == username); if (user is null) { throw new NotFoundException("User not found"); } - var usernameExists = context.Users.Any(r => r.Username == userDto.Username && r.Id != userId); + var usernameExists = _context.Users.Any(r => r.Username == userDto.Username && r.Id != user.Id); if (usernameExists) { throw new BadRequestException("User with given username already exists"); } - var emailExists = context.Users.Any(r => r.Email == userDto.Email && r.Id != userId); + var emailExists = _context.Users.Any(r => r.Email == userDto.Email && r.Id != user.Id); if (emailExists) { throw new BadRequestException("User with given email already exists"); } - var phoneExists = context.Users.Any(r => r.Phone == userDto.Phone && r.Id != userId); + var phoneExists = _context.Users.Any(r => r.Phone == userDto.Phone && r.Id != user.Id); if (phoneExists) { throw new BadRequestException("User with given phone number already exists"); } - user.Username = userDto.Username; user.Name = userDto.Name; user.Surname = userDto.Surname; user.Username = userDto.Username; user.Email = userDto.Email; user.Phone = userDto.Phone; user.Role = userDto.Role; - user.AddressId = userDto.AddressId; user.UpdatedAt = DateTime.UtcNow; - context.Users.Update(user); + _context.Users.Update(user); + + // Get list of interests to remove + var interestsToRemove = user.UserInterests + .Where(ui => !userDto.Interests!.Exists(uid => uid.Interest.Name == ui.Interest.Name)); + + _context.UserInterests.RemoveRange(interestsToRemove); if (userDto.Interests is null || userDto.Interests.Count == 0) { - context.SaveChanges(); + _context.SaveChanges(); scope.Complete(); return userDto; } - var interestsToRemove = user.UserInterests - .Where(ui => !userDto.Interests.Exists(uid => uid.Interest.Name == ui.Interest.Name)); - - context.UserInterests.RemoveRange(interestsToRemove); - - var interestsToAdd = userDto.Interests - .Where(uid => !user.UserInterests.Any(ui => ui.Interest.Name == uid.Interest.Name)) - .Select(uid => uid.ToEntity()) - .ToList(); - - context.UserInterests.AddRange(interestsToAdd); - + // Get list of interests to update var interestsToUpdate = user.UserInterests .Where(ui => userDto.Interests.Exists(uid => uid.Interest.Name == ui.Interest.Name)) .ToList(); @@ -92,10 +126,24 @@ public UserDto UpdateUser(int userId, UserDto userDto) } interest.Level = updatedInterest.Level; - context.UserInterests.Update(interest); + _context.UserInterests.Update(interest); } - context.SaveChanges(); + // Get list of existing interests in the database + var existingInterests = _context.Interests.ToList(); + + // Get list of interests to add + // Look for interests that are not already in the user's interests + var interestsToAdd = userDto.Interests + .Where(uid => !user.UserInterests.Any(ui => ui.Interest.Name == uid.Interest.Name)) + .Select(uid => uid.ToEntity(user.Id, existingInterests.Find(ei => ei.Name == uid.Interest.Name)!.Id)) + .ToList(); + + // Update interests for + interestsToAdd.ForEach(user.UserInterests.Add); + _context.Users.Update(user); + + _context.SaveChanges(); scope.Complete(); } @@ -104,8 +152,9 @@ public UserDto UpdateUser(int userId, UserDto userDto) public UserDto GetUserById(int userId) { - var user = context.Users + var user = _context.Users .Include(u => u.UserInterests) + .ThenInclude(ui => ui.Interest) .FirstOrDefault(u => u.Id == userId); if (user is null) @@ -118,8 +167,9 @@ public UserDto GetUserById(int userId) public UserDto GetUserByUsername(string username) { - var user = context.Users + var user = _context.Users .Include(u => u.UserInterests) + .ThenInclude(ui => ui.Interest) .FirstOrDefault(u => u.Username == username); if (user is null) @@ -132,7 +182,7 @@ public UserDto GetUserByUsername(string username) public IEnumerable GetUsersByFilter(Expression> filter) { - return context.Users + return _context.Users .Include(u => u.UserInterests) .ThenInclude(ui => ui.Interest) .Where(filter) @@ -142,7 +192,7 @@ public IEnumerable GetUsersByFilter(Expression> filter public IEnumerable GetAllUsers() { - var users = context.Users + var users = _context.Users .Include(u => u.UserInterests) .ThenInclude(ui => ui.Interest) .ToList(); diff --git a/realeated.py b/realeated.py new file mode 100644 index 00000000..58be0841 --- /dev/null +++ b/realeated.py @@ -0,0 +1,117 @@ +import psycopg2 +from sentence_transformers import SentenceTransformer +from sklearn.metrics.pairwise import cosine_similarity + +def connect_to_db(): + conn = psycopg2.connect( + dbname="reasn", + user="dba", + password="sql", + host="localhost", + port="5432" + ) + return conn + +def get_translated_tags_and_interests(conn): + with conn.cursor() as cur: + query_translated_tags = """ + SELECT t.name, tr.name_ang + FROM events.tag t + JOIN common.translated tr ON t.name = tr.name_pl + """ + query_translated_interests = """ + SELECT i.name, tr.name_ang + FROM users.interest i + JOIN common.translated tr ON i.name = tr.name_pl + """ + cur.execute(query_translated_tags) + tags = cur.fetchall() + + cur.execute(query_translated_interests) + interests = cur.fetchall() + + return tags, interests + +def get_missing_tags_and_interests(conn): + with conn.cursor() as cur: + query_missing_tags = """ + SELECT name + FROM events.tag + WHERE name NOT IN ( + SELECT tag_name + FROM common.related + ); + """ + query_missing_interests = """ + SELECT name + FROM users.interest + WHERE name NOT IN ( + SELECT interest_name + FROM common.related + ); + """ + cur.execute(query_missing_tags) + missing_tags = [row[0] for row in cur.fetchall()] + + cur.execute(query_missing_interests) + missing_interests = [row[0] for row in cur.fetchall()] + + return missing_tags, missing_interests + +def calculate_semantic_similarity_sbert(interests, tags): + model = SentenceTransformer('all-MiniLM-L6-v2') + + interest_embeddings = model.encode(interests) + tag_embeddings = model.encode(tags) + + similarities = cosine_similarity(interest_embeddings, tag_embeddings) + + return similarities + +def save_similarities_to_db(conn, tags, interests, similarities): + with conn.cursor() as cur: + insert_query = """ + INSERT INTO common.related (tag_name, interest_name, value) + VALUES (%s, %s, %s) + ON CONFLICT (tag_name, interest_name) DO NOTHING + """ + for i, tag in enumerate(tags): + for j, interest in enumerate(interests): + value = float(similarities[j][i]) + cur.execute(insert_query, (tag, interest, value)) + conn.commit() + +def main(): + conn = connect_to_db() + try: + # Pobierz przetĹ‚umaczone tagi i zainteresowania + translated_tags, translated_interests = get_translated_tags_and_interests(conn) + + # Rozdziel wyniki na oryginalne i przetĹ‚umaczone nazwy + tags, tags_ang = zip(*translated_tags) if translated_tags else ([], []) + interests, interests_ang = zip(*translated_interests) if translated_interests else ([], []) + + # Pobierz brakujÄ…ce tagi i zainteresowania + missing_tags, missing_interests = get_missing_tags_and_interests(conn) + + print("Missing tags:") + for tag in missing_tags: + print(tag) + + print("\nMissing interests:") + for interest in missing_interests: + print(interest) + + if interests_ang and missing_tags: + similarities_missing_tags = calculate_semantic_similarity_sbert(interests_ang, missing_tags) + save_similarities_to_db(conn, missing_tags, interests, similarities_missing_tags) + if missing_interests and tags_ang: + similarities_missing_interests = calculate_semantic_similarity_sbert(missing_interests, tags_ang) + save_similarities_to_db(conn, tags, missing_interests, similarities_missing_interests) + + print("\nSimilarities saved successfully.") + finally: + conn.close() + +if __name__ == "__main__": + main() diff --git a/recomendation.py b/recomendation.py new file mode 100644 index 00000000..0d5bd49b --- /dev/null +++ b/recomendation.py @@ -0,0 +1,58 @@ +from flask import Flask, request, jsonify +import psycopg2 +import psycopg2.extras + +app = Flask(__name__) + +def connect_to_db(): + conn = psycopg2.connect( + dbname="reasn", + user="dba", + password="sql", + host="localhost", + port="5432" + ) + return conn + +def get_similar_tags_from_db(conn, interests): + with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: + query = """ + SELECT DISTINCT tag_name + FROM common.related + WHERE interest_name = ANY(%s) AND value > 0.3 + """ + cur.execute(query, (interests,)) + results = cur.fetchall() + + if not results: + return None + + return [row['tag_name'] for row in results] + +@app.route('/similar-tags', methods=['POST']) +def get_similar_tags(): + data = request.get_json() + + if isinstance(data, list): + interests = data # Treat data as a list of interests directly + elif isinstance(data, dict): + interests = data.get('interests', []) + else: + return jsonify({'error': 'Invalid JSON data format'}), 400 + + if not interests: + return jsonify({'error': 'No interests provided'}), 400 + + conn = connect_to_db() + try: + similar_tags = get_similar_tags_from_db(conn, interests) + + if similar_tags is None: + return jsonify([]) + + return jsonify(similar_tags) + finally: + conn.close() + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file diff --git a/translated.py b/translated.py new file mode 100644 index 00000000..f3517506 --- /dev/null +++ b/translated.py @@ -0,0 +1,74 @@ +import psycopg2 +import psycopg2.extras +from deep_translator import GoogleTranslator + +def connect_to_db(): + conn = psycopg2.connect( + dbname="reasn", + user="dba", + password="sql", + host="localhost", + port="5432" + ) + return conn + +def get_untranslated_tags_and_interests(conn): + with conn.cursor() as cur: + query_tags = """ + SELECT DISTINCT name AS name + FROM events.tag + WHERE name NOT IN ( + SELECT name_pl + FROM common.translated + ); + """ + query_interests = """ + SELECT DISTINCT name AS name + FROM users.interest + WHERE name NOT IN ( + SELECT name_pl + FROM common.translated + ); + """ + cur.execute(query_tags) + tags = cur.fetchall() + cur.execute(query_interests) + interests = cur.fetchall() + + combined = set(tag[0] for tag in tags) | set(interest[0] for interest in interests) + return list(combined) + +def translate_to_english(texts): + translator = GoogleTranslator(source='auto', target='en') + translations = [translator.translate(text) for text in texts] + return translations + +def save_translations(conn, original_texts, translated_texts): + with conn.cursor() as cur: + insert_query = """ + INSERT INTO common.translated (name_pl, name_ang) + VALUES (%s, %s) + """ + for original, translated in zip(original_texts, translated_texts): + cur.execute(insert_query, (original, translated)) + conn.commit() + +def main(): + conn = connect_to_db() + try: + untranslated = get_untranslated_tags_and_interests(conn) + + print("Untranslated tags and interests:") + for item in untranslated: + print(item) + + translated = translate_to_english(untranslated) + + save_translations(conn, untranslated, translated) + + print("\nTranslations saved successfully.") + finally: + conn.close() + +if __name__ == "__main__": + main() \ No newline at end of file