I got this matching system which calculates the clusters and then choose if using a content based filtering or a collaborative filtering cluster-based. I'd like to make it smarter and better, more accurate even and with adjustments for each user without a full recluster
. package org.it.startup.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.it.startup.enums.SwipeType;
import org.it.startup.model.entity.User;
import org.it.startup.model.entity.UserSwipe;
import org.it.startup.repository.UserRepository;
import org.it.startup.repository.UserSwipeRepository;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import smile.clustering.KMeans;
import smile.feature.extraction.PCA;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Service
@Slf4j
@RequiredArgsConstructor
public class ClusteringService {
    private final UserRepository userRepository;
    private static final int NUM_CLUSTERS = 10;
    private final UserSwipeRepository userSwipeRepository;

    @Scheduled(cron = "0 0 0 * * *") // Runs daily at midnight
    @Transactional
    public void performClustering() {
        log.info("Starting clustering process...");
        List<User> users = userRepository.findAll().stream()
                .filter(user -> user.getEmbedding() != null)
                .collect(Collectors.toList());

        if (users.size() < NUM_CLUSTERS) {
            log.warn("Not enough users to form {} clusters.", NUM_CLUSTERS);
            return;
        }

        // Prepare data for clustering with additional features
        double[][] data = users.stream()
                .map(this::createFeatureVector)
                .toArray(double[][]::new);

        // Perform PCA to reduce dimensionality
        PCA pca = PCA.fit(data);
        data = pca.apply(data);

        // Perform K-Means clustering
        KMeans kMeans = KMeans.fit(data, NUM_CLUSTERS);

        int[] clusterLabels = kMeans.y;

        for (int i = 0; i < users.size(); i++) {
            User user = users.get(i);
            user.setCluster(clusterLabels[i]);
        }

        userRepository.saveAll(users);
        log.info("Clustering process completed. {} users clustered into {} groups.", users.size(), NUM_CLUSTERS);
    }

    private double[] createFeatureVector(User user) {
        float[] embedding = user.getEmbedding();
        int additionalFeaturesCount = 2; // Elo score and swipe count
        double[] featureVector = new double[embedding.length + additionalFeaturesCount];

        // Convert embedding to double
        for (int i = 0; i < embedding.length; i++) {
            featureVector[i] = embedding[i];
        }

        // Normalize additional features
        featureVector[embedding.length] = normalizeEloScore(user.getElo());
        featureVector[embedding.length + 1] = normalizeSwipeCount(Set.copyOf(userSwipeRepository.findBySwiper_Id(user.getId())));

        return featureVector;
    }

    private double normalizeSwipeCount(Set<UserSwipe> userSwipes) {
        long swipeRight = userSwipes.stream().filter(swipe -> swipe.getType() == SwipeType.POSITIVE).count();
        long swipeLeft = userSwipes.stream().filter(swipe -> swipe.getType() == SwipeType.NEGATIVE).count();
        long totalSwipes = swipeRight + swipeLeft;

        if (totalSwipes == 0) return 0.0;
        return (double) swipeRight / totalSwipes;
    }

    private double normalizeEloScore(Integer eloScore) {
        if (eloScore == null) return 0.0;
        double maxElo = 3000.0;
        return Math.min(eloScore, maxElo) / maxElo;
    }
}
package org.it.startup.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.it.startup.model.entity.User;
import org.it.startup.repository.UserRepository;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Slf4j
public class CollaborativeFilteringService {
    private final UserRepository userRepository;
    private final ClusteringService clusteringService;

    private static final int MIN_USERS_FOR_CLUSTERING = 20; // Threshold for collaborative matching

    /**
     * Finds potential matches for a given user.
     *
     * @param userId The ID of the user for whom to find matches.
     * @return A list of matched users.
     */
    public List<User> findMatches(Integer userId) {
        Optional<User> optionalUser = userRepository.findById(userId);
        if (optionalUser.isEmpty()) {
            log.warn("User with ID {} not found.", userId);
            return Collections.emptyList();
        }

        User user = optionalUser.get();

        // Check if there's enough data for clustering
        long activeUsersCount = userRepository.countByIdAndEmbeddingNotNull(userId); // Only active users
        if (activeUsersCount >= MIN_USERS_FOR_CLUSTERING) {
            // Collaborative Matching
            return collaborativeMatching(user);
        } else {
            // Fallback to Content-Based Matching
            return contentBasedMatching(user);
        }
    }

    /**
     * Collaborative filtering based on clustering.
     *
     * @param user The user for whom to find matches.
     * @return A list of matched users from the same cluster.
     */
    private List<User> collaborativeMatching(User user) {
        Integer userCluster = user.getCluster();
        if (userCluster == null) {
            log.warn("User {} does not belong to any cluster.", user.getId());
            return contentBasedMatching(user);
        }

        // Fetch users from the same cluster excluding the user itself

        // Optionally, sort or filter based on interaction metrics
        return userRepository.findByCluster(userCluster).stream()
                .filter(u -> !u.getId().equals(user.getId()))
                .collect(Collectors.toList());
    }

    /**
     * Content-based filtering using user embeddings and attributes.
     *
     * @param user The user for whom to find matches.
     * @return A list of matched users based on content similarity.
     */
    private List<User> contentBasedMatching(User user) {
        List<User> candidates = userRepository.findAll().stream()
                .filter(u -> !u.getId().equals(user.getId()) && u.getEmbedding() != null)
                .toList();

        // Calculate similarity scores
        Map<User, Double> similarityMap = new HashMap<>();

        for (User candidate : candidates) {
            similarityMap.put(candidate, enhancedSimilarity(user, candidate));
        }

        // Sort candidates by similarity score in descending order
        return similarityMap.entrySet().stream()
                .sorted(Map.Entry.<User, Double>comparingByValue().reversed())
                .limit(10) // Limit to top 10 matches
                .map(Map.Entry::getKey)
                .collect(Collectors.toList());
    }

    private double enhancedSimilarity(User user, User candidate) {
        double embeddingSimilarity = userRepository.doCosineSimilarity(user.getId(), candidate.getId());
        double eloSimilarity = 1.0 - Math.abs(user.getElo() - candidate.getElo()) / 3000.0;

        // Example weights
        double weightEmbedding = 0.7;
        double weightElo = 0.3;

        return (weightEmbedding * embeddingSimilarity) + (weightElo * eloSimilarity);
    }
}