From a826a70c688e051794217b07f61e5311c6c8926a Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 10 Jun 2026 18:38:05 +0200 Subject: [PATCH] Added distance to previous in the recommendation outputs --- .../AudioSimilarityEngine.impl.hpp | 69 ++++++++++--------- .../impl/clusters/ClustersEngine.cpp | 10 +-- .../include/services/recommendation/Types.hpp | 3 +- src/libs/subsonic/impl/endpoints/Browsing.cpp | 4 +- .../recommendation/LmsRecommendation.cpp | 14 ++-- 5 files changed, 54 insertions(+), 46 deletions(-) diff --git a/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp b/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp index 80960b9f..92a28801 100644 --- a/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp +++ b/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp @@ -64,21 +64,27 @@ namespace lms::recommendation { namespace detail { + struct TrackNeighbor + { + db::TrackId id; + float distance{}; + }; + template - TrackResults findNearestNeighbors( + std::vector findNearestNeighbors( const ReducedVector& queryVector, // expected to be normalized const std::unordered_map& trackVectors, std::size_t maxNeighbors, - db::TrackId excludeTrackId) + std::span excludeTrackIds) { const math::NormalizedCosineDistance distFunc{ queryVector }; - TrackResults neighbors; + std::vector neighbors; neighbors.reserve(trackVectors.size()); for (const auto& [trackId, trackVector] : trackVectors) { - if (trackId == excludeTrackId) + if (std::find(std::cbegin(excludeTrackIds), std::cend(excludeTrackIds), trackId) != std::cend(excludeTrackIds)) continue; neighbors.push_back({ .id = trackId, .distance = distFunc(*trackVector) }); @@ -186,27 +192,10 @@ namespace lms::recommendation return res; const ReducedVector& queryVector{ *medoidCalculator.finalize() }; - const math::NormalizedCosineDistance distFunc{ queryVector }; - - using Distance = float; - std::vector> rankedTracks; - rankedTracks.reserve(_trackVectors.size()); - - for (const auto& [trackId, vectors] : _trackVectors) - { - if (std::find(std::cbegin(tracksId), std::cend(tracksId), trackId) != std::cend(tracksId)) - continue; - - rankedTracks.emplace_back(trackId, distFunc(*vectors)); - } // Oversample to give the diversity selection enough candidates to work with static constexpr std::size_t oversamplingFactor{ 5 }; - const std::size_t candidateCount{ std::min(maxCount * oversamplingFactor, rankedTracks.size()) }; - std::partial_sort(std::begin(rankedTracks), std::next(std::begin(rankedTracks), static_cast(candidateCount)), std::end(rankedTracks), [](const auto& lhs, const auto& rhs) { - return lhs.second < rhs.second; - }); - rankedTracks.resize(candidateCount); + auto rankedTracks{ detail::findNearestNeighbors(queryVector, _trackVectors, maxCount * oversamplingFactor, tracksId) }; // Greedy selection: at each step pick the candidate with the lowest penalized score. // Pre-seed selectedTracks with the input tracks so that soft constraints (same release, @@ -216,6 +205,17 @@ namespace lms::recommendation selectedTracks.reserve(selectedTracks.size() + maxCount); res.reserve(maxCount); + const ReducedVector* prevVector{ nullptr }; + for (auto it{ tracksId.rbegin() }; it != tracksId.rend(); ++it) + { + const auto found{ _trackVectors.find(*it) }; + if (found != _trackVectors.cend()) + { + prevVector = found->second; + break; + } + } + while (res.size() < maxCount && !rankedTracks.empty()) { std::optional bestIdx; @@ -223,7 +223,7 @@ namespace lms::recommendation for (std::size_t i{}; i < rankedTracks.size(); ++i) { - const db::TrackId candidateId{ rankedTracks[i].first }; + const db::TrackId candidateId{ rankedTracks[i].id }; const TrackCandidateContext context{ .candidateTrackId = candidateId, @@ -246,8 +246,11 @@ namespace lms::recommendation break; const auto& [selectedId, distanceToQuery]{ rankedTracks[*bestIdx] }; - res.push_back({ .id = selectedId, .distance = distanceToQuery }); + const auto* selectedVector{ _trackVectors.at(selectedId) }; + const float distanceToPrev{ prevVector ? math::NormalizedCosineDistance{ *prevVector }(*selectedVector) : distanceToQuery }; + res.push_back({ .id = selectedId, .distanceToFirst = distanceToQuery, .distanceToPrevious = distanceToPrev }); selectedTracks.push_back(selectedId); + prevVector = selectedVector; rankedTracks.erase(std::begin(rankedTracks) + static_cast(*bestIdx)); } @@ -284,17 +287,17 @@ namespace lms::recommendation auto queryPoint{ startVector + direction * t }; queryPoint.normalizeL2(); - const auto neighbors{ detail::findNearestNeighbors(queryPoint, _trackVectors, NeighborCount, endTrackId) }; + const auto neighbors{ detail::findNearestNeighbors(queryPoint, _trackVectors, NeighborCount, std::span{ &endTrackId, 1 }) }; const db::TrackId stepSeedTrackId{ neighbors.empty() ? startTrackId : neighbors[0].id }; const std::array stepSeedTrackIds{ stepSeedTrackId }; std::optional best; float bestScore{ std::numeric_limits::max() }; - for (const auto& [candidateTrackId, candidateDistance] : neighbors) + for (const auto& neighbor : neighbors) { const TrackCandidateContext context{ - .candidateTrackId = candidateTrackId, + .candidateTrackId = neighbor.id, .selectedTracks = path, .seedTrackIds = stepSeedTrackIds, }; @@ -306,7 +309,7 @@ namespace lms::recommendation if (score < bestScore) { bestScore = score; - best = candidateTrackId; + best = neighbor.id; } } @@ -323,10 +326,14 @@ namespace lms::recommendation results.reserve(path.size()); const math::NormalizedCosineDistance startDistFunc{ startVector }; + const ReducedVector* prevVector{ &startVector }; for (const db::TrackId trackId : path) { const auto* trackVector{ _trackVectors.at(trackId) }; - results.push_back({ .id = trackId, .distance = startDistFunc(*trackVector) }); + const float distToFirst{ startDistFunc(*trackVector) }; + const float distToPrev{ math::NormalizedCosineDistance{ *prevVector }(*trackVector) }; + results.push_back({ .id = trackId, .distanceToFirst = distToFirst, .distanceToPrevious = distToPrev }); + prevVector = trackVector; } return results; @@ -384,7 +391,7 @@ namespace lms::recommendation res.reserve(resultCount); for (std::size_t i{}; i < resultCount; ++i) - res.push_back({ .id = rankedReleases[i].first, .distance = rankedReleases[i].second }); + res.push_back({ .id = rankedReleases[i].first, .distanceToFirst = rankedReleases[i].second }); return res; } @@ -444,7 +451,7 @@ namespace lms::recommendation res.reserve(resultCount); for (std::size_t i{}; i < resultCount; ++i) - res.push_back({ .id = rankedArtists[i].first, .distance = rankedArtists[i].second }); + res.push_back({ .id = rankedArtists[i].first, .distanceToFirst = rankedArtists[i].second }); return res; } diff --git a/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp b/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp index 1efee5a5..86540518 100644 --- a/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp +++ b/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp @@ -86,7 +86,7 @@ namespace lms::recommendation ResultContainer res; res.reserve(resultCount); for (std::size_t i{}; i < resultCount; ++i) - res.push_back({ .id = overlapCounts[i].first, .distance = {} }); + res.push_back({ .id = overlapCounts[i].first, .distanceToFirst = {}, .distanceToPrevious = {} }); return res; } @@ -295,7 +295,7 @@ namespace lms::recommendation if (!bestIdx) break; - res.push_back({ .id = candidates[*bestIdx], .distance = {} }); + res.push_back({ .id = candidates[*bestIdx], .distanceToFirst = {}, .distanceToPrevious = {} }); selectedTracks.push_back(candidates[*bestIdx]); candidates.erase(std::begin(candidates) + static_cast(*bestIdx)); } @@ -364,7 +364,7 @@ namespace lms::recommendation return {}; if (startTrackId == endTrackId) - return { RecommendationResult{ .id = startTrackId, .distance = {} } }; + return { RecommendationResult{ .id = startTrackId, .distanceToFirst = {}, .distanceToPrevious = {} } }; db::Session& dbSession{ _db.getTLSSession() }; auto transaction{ dbSession.createReadTransaction() }; @@ -376,9 +376,9 @@ namespace lms::recommendation TrackResults res; res.reserve(std::min(maxCount, 2)); - res.push_back({ .id = startTrackId, .distance = {} }); + res.push_back({ .id = startTrackId, .distanceToFirst = {}, .distanceToPrevious = {} }); if (maxCount > 1) - res.push_back({ .id = endTrackId, .distance = {} }); + res.push_back({ .id = endTrackId, .distanceToFirst = {}, .distanceToPrevious = {} }); return res; } diff --git a/src/libs/services/recommendation/include/services/recommendation/Types.hpp b/src/libs/services/recommendation/include/services/recommendation/Types.hpp index bd08ec1e..169872e7 100644 --- a/src/libs/services/recommendation/include/services/recommendation/Types.hpp +++ b/src/libs/services/recommendation/include/services/recommendation/Types.hpp @@ -31,7 +31,8 @@ namespace lms::recommendation struct RecommendationResult { IdType id; - float distance{}; // normalized distance in [0, 1]: 0 = most similar, 1 = least similar + float distanceToFirst{}; // normalized [0, 1]: 0 = most similar, 1 = least similar (distance to seed/query) + float distanceToPrevious{}; // normalized [0, 1]: 0 = most similar, 1 = least similar (distance to preceding result) }; template diff --git a/src/libs/subsonic/impl/endpoints/Browsing.cpp b/src/libs/subsonic/impl/endpoints/Browsing.cpp index 0180043f..5092e535 100644 --- a/src/libs/subsonic/impl/endpoints/Browsing.cpp +++ b/src/libs/subsonic/impl/endpoints/Browsing.cpp @@ -696,7 +696,7 @@ namespace lms::api::subsonic if (track) { Response::Node& sonicMatchNode{ response.createArrayNode("sonicMatch") }; - sonicMatchNode.setAttribute("similarity", 1.0F - similarTrack.distance); + sonicMatchNode.setAttribute("similarity", 1.0F - similarTrack.distanceToFirst); sonicMatchNode.addChild("entry", createSongNode(context, track, context.getUser())); } } @@ -727,7 +727,7 @@ namespace lms::api::subsonic if (track) { Response::Node& sonicMatchNode{ response.createArrayNode("sonicMatch") }; - sonicMatchNode.setAttribute("similarity", 1.0F - pathTrack.distance); + sonicMatchNode.setAttribute("similarity", 1.0F - pathTrack.distanceToFirst); sonicMatchNode.addChild("entry", createSongNode(context, track, context.getUser())); } } diff --git a/src/tools/recommendation/LmsRecommendation.cpp b/src/tools/recommendation/LmsRecommendation.cpp index f24b7a42..825d8d34 100644 --- a/src/tools/recommendation/LmsRecommendation.cpp +++ b/src/tools/recommendation/LmsRecommendation.cpp @@ -86,7 +86,7 @@ namespace lms std::cout << "Processing track " << trackToString(trackId) << std::endl; for (const auto& similarTrack : recommendationService.findSimilarTracks(std::span{ &trackId, 1 }, maxCount)) - std::cout << "\t- " << similarTrack.distance << ", Similar track " << trackToString(similarTrack.id) << std::endl; + std::cout << "\t- " << "Similar track " << trackToString(similarTrack.id) << " (first: " << similarTrack.distanceToFirst << ", prev: " << similarTrack.distanceToPrevious << ")" << std::endl; } } @@ -128,7 +128,7 @@ namespace lms std::cout << "Processing release '" << releaseToString(releaseId) << "'" << std::endl; for (const auto& similarRelease : recommendationService.findSimilarReleases(releaseId, maxCount)) - std::cout << "\t- " << similarRelease.distance << ", Similar release " << releaseToString(similarRelease.id) << std::endl; + std::cout << "\t- " << "Similar release " << releaseToString(similarRelease.id) << " (first: " << similarRelease.distanceToFirst << ", prev: " << similarRelease.distanceToPrevious << ")" << std::endl; } } @@ -163,7 +163,7 @@ namespace lms std::cout << "Processing artist '" << artistToString(artistId) << "'" << std::endl; for (const auto& similarArtist : recommendationService.findSimilarArtists(artistId, { db::TrackArtistLinkType::Artist }, maxCount)) - std::cout << "\t- " << similarArtist.distance << ", Similar artist '" << artistToString(similarArtist.id) << "'" << std::endl; + std::cout << "\t- " << "Similar artist '" << artistToString(similarArtist.id) << "' (first: " << similarArtist.distanceToFirst << ", prev: " << similarArtist.distanceToPrevious << ")" << std::endl; } } @@ -229,7 +229,7 @@ namespace lms } for (const auto& similarTrack : similarTracks) - std::cout << "\t- " << similarTrack.distance << ", Similar track " << trackToString(similarTrack.id) << std::endl; + std::cout << "\t- " << "Similar track " << trackToString(similarTrack.id) << " (first: " << similarTrack.distanceToFirst << ", prev: " << similarTrack.distanceToPrevious << ")" << std::endl; } } @@ -290,7 +290,7 @@ namespace lms } for (const auto& similarRelease : similarReleases) - std::cout << "\t- " << similarRelease.distance << ", Similar release " << releaseToString(similarRelease.id) << std::endl; + std::cout << "\t- " << "Similar release " << releaseToString(similarRelease.id) << " (first: " << similarRelease.distanceToFirst << ", prev: " << similarRelease.distanceToPrevious << ")" << std::endl; } } @@ -345,7 +345,7 @@ namespace lms } for (const auto& similarArtist : similarArtists) - std::cout << "\t- " << similarArtist.distance << ", Similar artist '" << artistToString(similarArtist.id) << "'" << std::endl; + std::cout << "\t- " << "Similar artist '" << artistToString(similarArtist.id) << "' (first: " << similarArtist.distanceToFirst << ", prev: " << similarArtist.distanceToPrevious << ")" << std::endl; } } @@ -436,7 +436,7 @@ namespace lms for (std::size_t i{}; i < path.size(); ++i) { const auto& result{ path[i] }; - std::cout << "\t" << (i + 1) << ". " << trackToString(result.id) << " (distance: " << result.distance << ")" << std::endl; + std::cout << "\t" << (i + 1) << ". " << trackToString(result.id) << " (first: " << result.distanceToFirst << ", prev: " << result.distanceToPrevious << ")" << std::endl; } } }