github.com/zsuzhengdu/helm@v3.0.0-beta.3+incompatible/cmd/helm/search/search.go (about) 1 /* 2 Copyright The Helm Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 /*Package search provides client-side repository searching. 18 19 This supports building an in-memory search index based on the contents of 20 multiple repositories, and then using string matching or regular expressions 21 to find matches. 22 */ 23 package search 24 25 import ( 26 "path" 27 "regexp" 28 "sort" 29 "strings" 30 31 "github.com/Masterminds/semver" 32 33 "helm.sh/helm/pkg/repo" 34 ) 35 36 // Result is a search result. 37 // 38 // Score indicates how close it is to match. The higher the score, the longer 39 // the distance. 40 type Result struct { 41 Name string 42 Score int 43 Chart *repo.ChartVersion 44 } 45 46 // Index is a searchable index of chart information. 47 type Index struct { 48 lines map[string]string 49 charts map[string]*repo.ChartVersion 50 } 51 52 const sep = "\v" 53 54 // NewIndex creats a new Index. 55 func NewIndex() *Index { 56 return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartVersion{}} 57 } 58 59 // verSep is a separator for version fields in map keys. 60 const verSep = "$$" 61 62 // AddRepo adds a repository index to the search index. 63 func (i *Index) AddRepo(rname string, ind *repo.IndexFile, all bool) { 64 ind.SortEntries() 65 for name, ref := range ind.Entries { 66 if len(ref) == 0 { 67 // Skip chart names that have zero releases. 68 continue 69 } 70 // By convention, an index file is supposed to have the newest at the 71 // 0 slot, so our best bet is to grab the 0 entry and build the index 72 // entry off of that. 73 // Note: Do not use filePath.Join since on Windows it will return \ 74 // which results in a repo name that cannot be understood. 75 fname := path.Join(rname, name) 76 if !all { 77 i.lines[fname] = indstr(rname, ref[0]) 78 i.charts[fname] = ref[0] 79 continue 80 } 81 82 // If 'all' is set, then we go through all of the refs, and add them all 83 // to the index. This will generate a lot of near-duplicate entries. 84 for _, rr := range ref { 85 versionedName := fname + verSep + rr.Version 86 i.lines[versionedName] = indstr(rname, rr) 87 i.charts[versionedName] = rr 88 } 89 } 90 } 91 92 // All returns all charts in the index as if they were search results. 93 // 94 // Each will be given a score of 0. 95 func (i *Index) All() []*Result { 96 res := make([]*Result, len(i.charts)) 97 j := 0 98 for name, ch := range i.charts { 99 parts := strings.Split(name, verSep) 100 res[j] = &Result{ 101 Name: parts[0], 102 Chart: ch, 103 } 104 j++ 105 } 106 return res 107 } 108 109 // Search searches an index for the given term. 110 // 111 // Threshold indicates the maximum score a term may have before being marked 112 // irrelevant. (Low score means higher relevance. Golf, not bowling.) 113 // 114 // If regexp is true, the term is treated as a regular expression. Otherwise, 115 // term is treated as a literal string. 116 func (i *Index) Search(term string, threshold int, regexp bool) ([]*Result, error) { 117 if regexp { 118 return i.SearchRegexp(term, threshold) 119 } 120 return i.SearchLiteral(term, threshold), nil 121 } 122 123 // calcScore calculates a score for a match. 124 func (i *Index) calcScore(index int, matchline string) int { 125 126 // This is currently tied to the fact that sep is a single char. 127 splits := []int{} 128 s := rune(sep[0]) 129 for i, ch := range matchline { 130 if ch == s { 131 splits = append(splits, i) 132 } 133 } 134 135 for i, pos := range splits { 136 if index > pos { 137 continue 138 } 139 return i 140 } 141 return len(splits) 142 } 143 144 // SearchLiteral does a literal string search (no regexp). 145 func (i *Index) SearchLiteral(term string, threshold int) []*Result { 146 term = strings.ToLower(term) 147 buf := []*Result{} 148 for k, v := range i.lines { 149 lk := strings.ToLower(k) 150 lv := strings.ToLower(v) 151 res := strings.Index(lv, term) 152 if score := i.calcScore(res, lv); res != -1 && score < threshold { 153 parts := strings.Split(lk, verSep) // Remove version, if it is there. 154 buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) 155 } 156 } 157 return buf 158 } 159 160 // SearchRegexp searches using a regular expression. 161 func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) { 162 matcher, err := regexp.Compile(re) 163 if err != nil { 164 return []*Result{}, err 165 } 166 buf := []*Result{} 167 for k, v := range i.lines { 168 ind := matcher.FindStringIndex(v) 169 if len(ind) == 0 { 170 continue 171 } 172 if score := i.calcScore(ind[0], v); ind[0] >= 0 && score < threshold { 173 parts := strings.Split(k, verSep) // Remove version, if it is there. 174 buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) 175 } 176 } 177 return buf, nil 178 } 179 180 // SortScore does an in-place sort of the results. 181 // 182 // Lowest scores are highest on the list. Matching scores are subsorted alphabetically. 183 func SortScore(r []*Result) { 184 sort.Sort(scoreSorter(r)) 185 } 186 187 // scoreSorter sorts results by score, and subsorts by alpha Name. 188 type scoreSorter []*Result 189 190 // Len returns the length of this scoreSorter. 191 func (s scoreSorter) Len() int { return len(s) } 192 193 // Swap performs an in-place swap. 194 func (s scoreSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 195 196 // Less compares a to b, and returns true if a is less than b. 197 func (s scoreSorter) Less(a, b int) bool { 198 first := s[a] 199 second := s[b] 200 201 if first.Score > second.Score { 202 return false 203 } 204 if first.Score < second.Score { 205 return true 206 } 207 if first.Name == second.Name { 208 v1, err := semver.NewVersion(first.Chart.Version) 209 if err != nil { 210 return true 211 } 212 v2, err := semver.NewVersion(second.Chart.Version) 213 if err != nil { 214 return true 215 } 216 // Sort so that the newest chart is higher than the oldest chart. This is 217 // the opposite of what you'd expect in a function called Less. 218 return v1.GreaterThan(v2) 219 } 220 return first.Name < second.Name 221 } 222 223 func indstr(name string, ref *repo.ChartVersion) string { 224 i := ref.Name + sep + name + "/" + ref.Name + sep + 225 ref.Description + sep + strings.Join(ref.Keywords, " ") 226 return i 227 }