ARPCorrelationAlgorithms.cpp (9261B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "winget/ARPCorrelationAlgorithms.h" 5 6 using namespace AppInstaller::Manifest; 7 using namespace AppInstaller::Repository; 8 using namespace AppInstaller::Utility; 9 10 namespace AppInstaller::Repository::Correlation 11 { 12 using WordSequence = WordsEditDistanceMatchConfidenceAlgorithm::WordSequence; 13 14 namespace 15 { 16 // A simple matrix class to hold score tables without having to allocate multiple arrays. 17 struct Matrix 18 { 19 Matrix(size_t rows, size_t columns) : m_rows(rows), m_columns(columns), m_data(rows* columns) {} 20 21 double& At(size_t i, size_t j) 22 { 23 return m_data[i * m_columns + j]; 24 } 25 26 private: 27 size_t m_rows; 28 size_t m_columns; 29 std::vector<double> m_data; 30 }; 31 32 double EditDistanceScore(const std::vector<std::string>& s1, const std::vector<std::string>& s2) 33 { 34 // Naive implementation of edit distance (scaled over the sequence size). 35 // This considers only the operations of adding and removing elements. 36 37 if (s1.empty() || s2.empty()) 38 { 39 return 0; 40 } 41 42 // distance[i, j] = distance between s1[0:i] and s2[0:j] 43 // We don't need to hold more than two rows at a time, but it's simpler to keep the whole table. 44 Matrix distance(s1.size() + 1, s2.size() + 1); 45 46 for (size_t i = 0; i < s1.size(); ++i) 47 { 48 for (size_t j = 0; j < s2.size(); ++j) 49 { 50 double& d = distance.At(i, j); 51 if (s1[i] == s2[j]) 52 { 53 // If the two elements are equal, the distance is the same as from one element before. 54 // In case we are on the first element of one of the two sequences, the distance is 55 // equal to the cost of adding all the previous elements in the other 56 if (i == 0) 57 { 58 d = static_cast<double>(j); 59 } 60 else if (j == 0) 61 { 62 d = static_cast<double>(i); 63 } 64 else 65 { 66 d = distance.At(i - 1, j - 1); 67 } 68 } 69 else 70 { 71 // If the two elements are distinct, the score is the cost of removing the last element 72 // in one sequence plus the cost of editing the remainder of both. 73 if (i > 0 && j > 0) 74 { 75 d = 1 + std::min(distance.At(i - 1, j), distance.At(i, j - 1)); 76 } 77 else if (i > 0) 78 { 79 d = 1 + distance.At(i - 1, j); 80 } 81 else if (j > 0) 82 { 83 d = 1 + distance.At(i, j - 1); 84 } 85 else 86 { 87 // Remove one and add the other 88 d = 2; 89 } 90 } 91 } 92 } 93 94 // Maximum distance is equal to the sum of both lengths (removing all elements from one and adding all the elements from the other). 95 // We use that to scale to [0,1]. 96 // A smaller distance represents a higher match, so we subtract from 1 for the final score 97 double editDistance = distance.At(s1.size() - 1, s2.size() - 1); 98 return 1 - editDistance / (static_cast<uint64_t>(s1.size()) + static_cast<uint64_t>(s2.size())); 99 } 100 } 101 102 WordsEditDistanceMatchConfidenceAlgorithm::NameAndPublisher::NameAndPublisher(const WordSequence& name, const WordSequence& publisher) : Name(name), Publisher(publisher) 103 { 104 NamePublisher.insert(NamePublisher.end(), publisher.begin(), publisher.end()); 105 NamePublisher.insert(NamePublisher.end(), name.begin(), name.end()); 106 } 107 108 WordsEditDistanceMatchConfidenceAlgorithm::NameAndPublisher::NameAndPublisher(WordSequence&& name, WordSequence&& publisher) : Name(std::move(name)), Publisher(std::move(publisher)) 109 { 110 NamePublisher.insert(NamePublisher.end(), publisher.begin(), publisher.end()); 111 NamePublisher.insert(NamePublisher.end(), name.begin(), name.end()); 112 113 } 114 115 void WordsEditDistanceMatchConfidenceAlgorithm::Init(const AppInstaller::Manifest::Manifest& manifest) 116 { 117 // We will use the name and publisher from each localization. 118 m_namesAndPublishers.clear(); 119 120 WordSequence defaultPublisher; 121 if (manifest.DefaultLocalization.Contains(Manifest::Localization::Publisher)) 122 { 123 defaultPublisher = NormalizeAndPreparePublisher(manifest.DefaultLocalization.Get<Manifest::Localization::Publisher>()); 124 } 125 126 if (manifest.DefaultLocalization.Contains(Manifest::Localization::PackageName)) 127 { 128 WordSequence defaultName = NormalizeAndPrepareName(manifest.DefaultLocalization.Get<Manifest::Localization::PackageName>()); 129 m_namesAndPublishers.emplace_back(defaultName, defaultPublisher); 130 131 for (const auto& loc : manifest.Localizations) 132 { 133 if (loc.Contains(Manifest::Localization::PackageName) || loc.Contains(Manifest::Localization::Publisher)) 134 { 135 auto name = loc.Contains(Manifest::Localization::PackageName) ? NormalizeAndPrepareName(loc.Get<Manifest::Localization::PackageName>()) : defaultName; 136 auto publisher = loc.Contains(Manifest::Localization::Publisher) ? NormalizeAndPreparePublisher(loc.Get<Manifest::Localization::Publisher>()) : defaultPublisher; 137 138 m_namesAndPublishers.emplace_back(std::move(name), std::move(publisher)); 139 } 140 } 141 } 142 } 143 144 double WordsEditDistanceMatchConfidenceAlgorithm::ComputeConfidence(const ARPEntry& arpEntry) const 145 { 146 // Name and Publisher are available as multi properties, but for ARP entries there will only be 0 or 1 values. 147 NameAndPublisher arpNameAndPublisher( 148 NormalizeAndPrepareName(arpEntry.Entry->GetLatestVersion()->GetProperty(PackageVersionProperty::Name).get()), 149 NormalizeAndPreparePublisher(arpEntry.Entry->GetLatestVersion()->GetProperty(PackageVersionProperty::Publisher).get())); 150 151 // Get the best score across all localizations 152 double bestMatchingScore = 0; 153 for (const auto& manifestNameAndPublisher : m_namesAndPublishers) 154 { 155 // Sometimes the publisher may be included in the name, for example Microsoft PowerToys as opposed to simply PowerToys. 156 // This may happen both in the ARP entry and the manifest. We try adding it in case it is in one but not in both. 157 auto nameScore = EditDistanceScore(manifestNameAndPublisher.Name, arpNameAndPublisher.Name); 158 159 // Ignore cases where the name is not at all similar to avoid matching due to publisher only 160 if (nameScore < m_nameMatchingScoreMinThreshold) 161 { 162 continue; 163 } 164 165 auto publisherScore = EditDistanceScore(manifestNameAndPublisher.Publisher, arpNameAndPublisher.Publisher); 166 auto namePublisherScore = std::max( 167 EditDistanceScore(manifestNameAndPublisher.NamePublisher, arpNameAndPublisher.Name), 168 EditDistanceScore(manifestNameAndPublisher.Name, arpNameAndPublisher.NamePublisher)); 169 170 // Use the best between considering name and publisher as a single string or separately. 171 auto score = std::max( 172 nameScore * m_nameMatchingScoreWeight + publisherScore * (1 - m_nameMatchingScoreWeight), 173 namePublisherScore); 174 bestMatchingScore = std::max(bestMatchingScore, score); 175 } 176 177 // Factor in whether this entry is new 178 auto result = bestMatchingScore * m_stringMatchingWeight + (arpEntry.IsNewOrUpdated ? 1 : 0) * (1 - m_stringMatchingWeight); 179 180 return result; 181 } 182 183 WordSequence WordsEditDistanceMatchConfidenceAlgorithm::PrepareString(std::string_view s) const 184 { 185 return Utility::SplitIntoWords(Utility::FoldCase(s)); 186 } 187 188 WordSequence WordsEditDistanceMatchConfidenceAlgorithm::NormalizeAndPrepareName(std::string_view name) const 189 { 190 return PrepareString(m_normalizer.NormalizeName(name).Name()); 191 } 192 193 WordSequence WordsEditDistanceMatchConfidenceAlgorithm::NormalizeAndPreparePublisher(std::string_view publisher) const 194 { 195 return PrepareString(m_normalizer.NormalizePublisher(publisher)); 196 } 197 }