github.com/rakanixu/helm@v2.8.2+incompatible/cmd/helm/search/search.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors All rights reserved. 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 "errors" 27 "path" 28 "regexp" 29 "sort" 30 "strings" 31 32 "github.com/Masterminds/semver" 33 "k8s.io/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 // Chart returns the ChartVersion for a particular name. 181 func (i *Index) Chart(name string) (*repo.ChartVersion, error) { 182 c, ok := i.charts[name] 183 if !ok { 184 return nil, errors.New("no such chart") 185 } 186 return c, nil 187 } 188 189 // SortScore does an in-place sort of the results. 190 // 191 // Lowest scores are highest on the list. Matching scores are subsorted alphabetically. 192 func SortScore(r []*Result) { 193 sort.Sort(scoreSorter(r)) 194 } 195 196 // scoreSorter sorts results by score, and subsorts by alpha Name. 197 type scoreSorter []*Result 198 199 // Len returns the length of this scoreSorter. 200 func (s scoreSorter) Len() int { return len(s) } 201 202 // Swap performs an in-place swap. 203 func (s scoreSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 204 205 // Less compares a to b, and returns true if a is less than b. 206 func (s scoreSorter) Less(a, b int) bool { 207 first := s[a] 208 second := s[b] 209 210 if first.Score > second.Score { 211 return false 212 } 213 if first.Score < second.Score { 214 return true 215 } 216 if first.Name == second.Name { 217 v1, err := semver.NewVersion(first.Chart.Version) 218 if err != nil { 219 return true 220 } 221 v2, err := semver.NewVersion(second.Chart.Version) 222 if err != nil { 223 return true 224 } 225 // Sort so that the newest chart is higher than the oldest chart. This is 226 // the opposite of what you'd expect in a function called Less. 227 return v1.GreaterThan(v2) 228 } 229 return first.Name < second.Name 230 } 231 232 func indstr(name string, ref *repo.ChartVersion) string { 233 i := ref.Name + sep + name + "/" + ref.Name + sep + 234 ref.Description + sep + strings.Join(ref.Keywords, " ") 235 return i 236 }