mixed_sources.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. package query
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "sort"
  7. "strconv"
  8. "strings"
  9. "github.com/Jguer/aur"
  10. "github.com/Jguer/go-alpm/v2"
  11. "github.com/adrg/strutil"
  12. "github.com/adrg/strutil/metrics"
  13. "github.com/leonelquinteros/gotext"
  14. "github.com/Jguer/yay/v11/pkg/db"
  15. "github.com/Jguer/yay/v11/pkg/intrange"
  16. "github.com/Jguer/yay/v11/pkg/settings/parser"
  17. "github.com/Jguer/yay/v11/pkg/stringset"
  18. "github.com/Jguer/yay/v11/pkg/text"
  19. )
  20. const sourceAUR = "aur"
  21. type Builder interface {
  22. Len() int
  23. Execute(ctx context.Context, dbExecutor db.Executor, aurClient *aur.Client, pkgS []string)
  24. Results(w io.Writer, dbExecutor db.Executor, verboseSearch SearchVerbosity) error
  25. GetTargets(include, exclude intrange.IntRanges, otherExclude stringset.StringSet) ([]string, error)
  26. }
  27. type MixedSourceQueryBuilder struct {
  28. results []abstractResult
  29. sortBy string
  30. searchBy string
  31. targetMode parser.TargetMode
  32. queryMap map[string]map[string]interface{}
  33. bottomUp bool
  34. singleLineResults bool
  35. }
  36. func NewMixedSourceQueryBuilder(
  37. sortBy string,
  38. targetMode parser.TargetMode,
  39. searchBy string,
  40. bottomUp,
  41. singleLineResults bool,
  42. ) *MixedSourceQueryBuilder {
  43. return &MixedSourceQueryBuilder{
  44. bottomUp: bottomUp,
  45. sortBy: sortBy,
  46. targetMode: targetMode,
  47. searchBy: searchBy,
  48. singleLineResults: singleLineResults,
  49. queryMap: map[string]map[string]interface{}{},
  50. results: make([]abstractResult, 0, 100),
  51. }
  52. }
  53. type abstractResult struct {
  54. source string
  55. name string
  56. description string
  57. votes int
  58. provides []string
  59. }
  60. type abstractResults struct {
  61. results []abstractResult
  62. search string
  63. distanceCache map[string]float64
  64. bottomUp bool
  65. metric strutil.StringMetric
  66. }
  67. func (a *abstractResults) Len() int { return len(a.results) }
  68. func (a *abstractResults) Swap(i, j int) { a.results[i], a.results[j] = a.results[j], a.results[i] }
  69. func (a *abstractResults) GetMetric(pkg *abstractResult) float64 {
  70. if v, ok := a.distanceCache[pkg.name]; ok {
  71. return v
  72. }
  73. sim := strutil.Similarity(pkg.name, a.search, a.metric)
  74. for _, prov := range pkg.provides {
  75. // If the package provides search, it's a perfect match
  76. // AUR packages don't populate provides
  77. candidate := strutil.Similarity(prov, a.search, a.metric)
  78. if candidate > sim {
  79. sim = candidate
  80. }
  81. }
  82. simDesc := strutil.Similarity(pkg.description, a.search, a.metric)
  83. // slightly overweight sync sources by always giving them max popularity
  84. popularity := 1.0
  85. if pkg.source == sourceAUR {
  86. popularity = float64(pkg.votes) / float64(pkg.votes+60)
  87. }
  88. sim = sim*0.6 + simDesc*0.2 + popularity*0.2
  89. a.distanceCache[pkg.name] = sim
  90. return sim
  91. }
  92. func (a *abstractResults) Less(i, j int) bool {
  93. pkgA := a.results[i]
  94. pkgB := a.results[j]
  95. simA := a.GetMetric(&pkgA)
  96. simB := a.GetMetric(&pkgB)
  97. if a.bottomUp {
  98. return simA < simB
  99. }
  100. return simA > simB
  101. }
  102. func (s *MixedSourceQueryBuilder) Execute(ctx context.Context, dbExecutor db.Executor, aurClient *aur.Client, pkgS []string) {
  103. var aurErr error
  104. pkgS = RemoveInvalidTargets(pkgS, s.targetMode)
  105. metric := &metrics.JaroWinkler{
  106. CaseSensitive: false,
  107. }
  108. sortableResults := &abstractResults{
  109. results: []abstractResult{},
  110. search: strings.Join(pkgS, ""),
  111. distanceCache: map[string]float64{},
  112. bottomUp: s.bottomUp,
  113. metric: metric,
  114. }
  115. if s.targetMode.AtLeastAUR() {
  116. var aurResults aurQuery
  117. aurResults, aurErr = queryAUR(ctx, aurClient, pkgS, s.searchBy)
  118. dbName := sourceAUR
  119. for i := range aurResults {
  120. if s.queryMap[dbName] == nil {
  121. s.queryMap[dbName] = map[string]interface{}{}
  122. }
  123. s.queryMap[dbName][aurResults[i].Name] = aurResults[i]
  124. sortableResults.results = append(sortableResults.results, abstractResult{
  125. source: dbName,
  126. name: aurResults[i].Name,
  127. description: aurResults[i].Description,
  128. provides: aurResults[i].Provides,
  129. votes: aurResults[i].NumVotes,
  130. })
  131. }
  132. }
  133. var repoResults []alpm.IPackage
  134. if s.targetMode.AtLeastRepo() {
  135. repoResults = dbExecutor.SyncPackages(pkgS...)
  136. for i := range repoResults {
  137. dbName := repoResults[i].DB().Name()
  138. if s.queryMap[dbName] == nil {
  139. s.queryMap[dbName] = map[string]interface{}{}
  140. }
  141. s.queryMap[dbName][repoResults[i].Name()] = repoResults[i]
  142. rawProvides := repoResults[i].Provides().Slice()
  143. provides := make([]string, len(rawProvides))
  144. for j := range rawProvides {
  145. provides[j] = rawProvides[j].Name
  146. }
  147. sortableResults.results = append(sortableResults.results, abstractResult{
  148. source: repoResults[i].DB().Name(),
  149. name: repoResults[i].Name(),
  150. description: repoResults[i].Description(),
  151. provides: provides,
  152. votes: -1,
  153. })
  154. }
  155. }
  156. sort.Sort(sortableResults)
  157. s.results = sortableResults.results
  158. if aurErr != nil {
  159. text.Errorln(ErrAURSearch{inner: aurErr})
  160. if len(repoResults) != 0 {
  161. text.Warnln(gotext.Get("Showing repo packages only"))
  162. }
  163. }
  164. }
  165. func (s *MixedSourceQueryBuilder) Results(w io.Writer, dbExecutor db.Executor, verboseSearch SearchVerbosity) error {
  166. for i := range s.results {
  167. if verboseSearch == Minimal {
  168. _, _ = fmt.Fprintln(w, s.results[i].name)
  169. continue
  170. }
  171. var toPrint string
  172. if verboseSearch == NumberMenu {
  173. if s.bottomUp {
  174. toPrint += text.Magenta(strconv.Itoa(len(s.results)-i)) + " "
  175. } else {
  176. toPrint += text.Magenta(strconv.Itoa(i+1)) + " "
  177. }
  178. }
  179. pkg := s.queryMap[s.results[i].source][s.results[i].name]
  180. if s.results[i].source == sourceAUR {
  181. aurPkg := pkg.(aur.Pkg)
  182. toPrint += aurPkgSearchString(&aurPkg, dbExecutor, s.singleLineResults)
  183. } else {
  184. syncPkg := pkg.(alpm.IPackage)
  185. toPrint += syncPkgSearchString(syncPkg, dbExecutor, s.singleLineResults)
  186. }
  187. fmt.Fprintln(w, toPrint)
  188. }
  189. return nil
  190. }
  191. func (s *MixedSourceQueryBuilder) Len() int {
  192. return len(s.results)
  193. }
  194. func (s *MixedSourceQueryBuilder) GetTargets(include, exclude intrange.IntRanges,
  195. otherExclude stringset.StringSet,
  196. ) ([]string, error) {
  197. var (
  198. isInclude = len(exclude) == 0 && len(otherExclude) == 0
  199. targets []string
  200. lenRes = len(s.results)
  201. )
  202. for i := 0; i <= s.Len(); i++ {
  203. target := i - 1
  204. if s.bottomUp {
  205. target = lenRes - i
  206. }
  207. if (isInclude && include.Get(i)) || (!isInclude && !exclude.Get(i)) {
  208. targets = append(targets, s.results[target].source+"/"+s.results[target].name)
  209. }
  210. }
  211. return targets, nil
  212. }