github.com/zoumo/helm@v2.5.0+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 34 "k8s.io/helm/pkg/repo" 35 ) 36 37 // Result is a search result. 38 // 39 // Score indicates how close it is to match. The higher the score, the longer 40 // the distance. 41 type Result struct { 42 Name string 43 Score int 44 Chart *repo.ChartVersion 45 } 46 47 // Index is a searchable index of chart information. 48 type Index struct { 49 lines map[string]string 50 charts map[string]*repo.ChartVersion 51 } 52 53 const sep = "\v" 54 55 // NewIndex creats a new Index. 56 func NewIndex() *Index { 57 return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartVersion{}} 58 } 59 60 // verSep is a separator for version fields in map keys. 61 const verSep = "$$" 62 63 // AddRepo adds a repository index to the search index. 64 func (i *Index) AddRepo(rname string, ind *repo.IndexFile, all bool) { 65 ind.SortEntries() 66 for name, ref := range ind.Entries { 67 if len(ref) == 0 { 68 // Skip chart names that have zero releases. 69 continue 70 } 71 // By convention, an index file is supposed to have the newest at the 72 // 0 slot, so our best bet is to grab the 0 entry and build the index 73 // entry off of that. 74 // Note: Do not use filePath.Join since on Windows it will return \ 75 // which results in a repo name that cannot be understood. 76 fname := path.Join(rname, name) 77 if !all { 78 i.lines[fname] = indstr(rname, ref[0]) 79 i.charts[fname] = ref[0] 80 continue 81 } 82 83 // If 'all' is set, then we go through all of the refs, and add them all 84 // to the index. This will generate a lot of near-duplicate entries. 85 for _, rr := range ref { 86 versionedName := fname + verSep + rr.Version 87 i.lines[versionedName] = indstr(rname, rr) 88 i.charts[versionedName] = rr 89 } 90 } 91 } 92 93 // All returns all charts in the index as if they were search results. 94 // 95 // Each will be given a score of 0. 96 func (i *Index) All() []*Result { 97 res := make([]*Result, len(i.charts)) 98 j := 0 99 for name, ch := range i.charts { 100 parts := strings.Split(name, verSep) 101 res[j] = &Result{ 102 Name: parts[0], 103 Chart: ch, 104 } 105 j++ 106 } 107 return res 108 } 109 110 // Search searches an index for the given term. 111 // 112 // Threshold indicates the maximum score a term may have before being marked 113 // irrelevant. (Low score means higher relevance. Golf, not bowling.) 114 // 115 // If regexp is true, the term is treated as a regular expression. Otherwise, 116 // term is treated as a literal string. 117 func (i *Index) Search(term string, threshold int, regexp bool) ([]*Result, error) { 118 if regexp { 119 return i.SearchRegexp(term, threshold) 120 } 121 return i.SearchLiteral(term, threshold), nil 122 } 123 124 // calcScore calculates a score for a match. 125 func (i *Index) calcScore(index int, matchline string) int { 126 127 // This is currently tied to the fact that sep is a single char. 128 splits := []int{} 129 s := rune(sep[0]) 130 for i, ch := range matchline { 131 if ch == s { 132 splits = append(splits, i) 133 } 134 } 135 136 for i, pos := range splits { 137 if index > pos { 138 continue 139 } 140 return i 141 } 142 return len(splits) 143 } 144 145 // SearchLiteral does a literal string search (no regexp). 146 func (i *Index) SearchLiteral(term string, threshold int) []*Result { 147 term = strings.ToLower(term) 148 buf := []*Result{} 149 for k, v := range i.lines { 150 res := strings.Index(v, term) 151 if score := i.calcScore(res, v); res != -1 && score < threshold { 152 parts := strings.Split(k, verSep) // Remove version, if it is there. 153 buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) 154 } 155 } 156 return buf 157 } 158 159 // SearchRegexp searches using a regular expression. 160 func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) { 161 matcher, err := regexp.Compile(re) 162 if err != nil { 163 return []*Result{}, err 164 } 165 buf := []*Result{} 166 for k, v := range i.lines { 167 ind := matcher.FindStringIndex(v) 168 if len(ind) == 0 { 169 continue 170 } 171 if score := i.calcScore(ind[0], v); ind[0] >= 0 && score < threshold { 172 parts := strings.Split(k, verSep) // Remove version, if it is there. 173 buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) 174 } 175 } 176 return buf, nil 177 } 178 179 // Chart returns the ChartVersion for a particular name. 180 func (i *Index) Chart(name string) (*repo.ChartVersion, error) { 181 c, ok := i.charts[name] 182 if !ok { 183 return nil, errors.New("no such chart") 184 } 185 return c, nil 186 } 187 188 // SortScore does an in-place sort of the results. 189 // 190 // Lowest scores are highest on the list. Matching scores are subsorted alphabetically. 191 func SortScore(r []*Result) { 192 sort.Sort(scoreSorter(r)) 193 } 194 195 // scoreSorter sorts results by score, and subsorts by alpha Name. 196 type scoreSorter []*Result 197 198 // Len returns the length of this scoreSorter. 199 func (s scoreSorter) Len() int { return len(s) } 200 201 // Swap performs an in-place swap. 202 func (s scoreSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 203 204 // Less compares a to b, and returns true if a is less than b. 205 func (s scoreSorter) Less(a, b int) bool { 206 first := s[a] 207 second := s[b] 208 209 if first.Score > second.Score { 210 return false 211 } 212 if first.Score < second.Score { 213 return true 214 } 215 if first.Name == second.Name { 216 v1, err := semver.NewVersion(first.Chart.Version) 217 if err != nil { 218 return true 219 } 220 v2, err := semver.NewVersion(second.Chart.Version) 221 if err != nil { 222 return true 223 } 224 // Sort so that the newest chart is higher than the oldest chart. This is 225 // the opposite of what you'd expect in a function called Less. 226 return v1.GreaterThan(v2) 227 } 228 return first.Name < second.Name 229 } 230 231 func indstr(name string, ref *repo.ChartVersion) string { 232 i := ref.Name + sep + name + "/" + ref.Name + sep + 233 ref.Description + sep + strings.Join(ref.Keywords, " ") 234 return strings.ToLower(i) 235 }