github.com/hiddeco/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  }