github.com/sdbaiguanghe/helm@v2.16.7+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  	"errors"
    27  	"path"
    28  	"regexp"
    29  	"sort"
    30  	"strings"
    31  
    32  	"github.com/Masterminds/semver"
    33  	"k8s.io/helm/pkg/repo"
    34  )
    35  
    36  const (
    37  	sep = "\v"
    38  	// verSep is a separator for version fields in map keys.
    39  	verSep = "$$"
    40  )
    41  
    42  // Result is a search result.
    43  //
    44  // Score indicates how close it is to match. The higher the score, the longer
    45  // the distance.
    46  type Result struct {
    47  	Name  string
    48  	Score int
    49  	Chart *repo.ChartVersion
    50  }
    51  
    52  // Index is a searchable index of chart information.
    53  type Index struct {
    54  	lines  map[string]string
    55  	charts map[string]*repo.ChartVersion
    56  }
    57  
    58  // NewIndex creates a new Index.
    59  func NewIndex() *Index {
    60  	return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartVersion{}}
    61  }
    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  		lk := strings.ToLower(k)
   151  		lv := strings.ToLower(v)
   152  		res := strings.Index(lv, term)
   153  		if score := i.calcScore(res, lv); res != -1 && score < threshold {
   154  			parts := strings.Split(lk, verSep) // Remove version, if it is there.
   155  			buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]})
   156  		}
   157  	}
   158  	return buf
   159  }
   160  
   161  // SearchRegexp searches using a regular expression.
   162  func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) {
   163  	matcher, err := regexp.Compile(re)
   164  	if err != nil {
   165  		return []*Result{}, err
   166  	}
   167  	buf := []*Result{}
   168  	for k, v := range i.lines {
   169  		ind := matcher.FindStringIndex(v)
   170  		if len(ind) == 0 {
   171  			continue
   172  		}
   173  		if score := i.calcScore(ind[0], v); ind[0] >= 0 && score < threshold {
   174  			parts := strings.Split(k, verSep) // Remove version, if it is there.
   175  			buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]})
   176  		}
   177  	}
   178  	return buf, nil
   179  }
   180  
   181  // Chart returns the ChartVersion for a particular name.
   182  func (i *Index) Chart(name string) (*repo.ChartVersion, error) {
   183  	c, ok := i.charts[name]
   184  	if !ok {
   185  		return nil, errors.New("no such chart")
   186  	}
   187  	return c, nil
   188  }
   189  
   190  // SortScore does an in-place sort of the results.
   191  //
   192  // Lowest scores are highest on the list. Matching scores are subsorted alphabetically.
   193  func SortScore(r []*Result) {
   194  	sort.Sort(scoreSorter(r))
   195  }
   196  
   197  // scoreSorter sorts results by score, and subsorts by alpha Name.
   198  type scoreSorter []*Result
   199  
   200  // Len returns the length of this scoreSorter.
   201  func (s scoreSorter) Len() int { return len(s) }
   202  
   203  // Swap performs an in-place swap.
   204  func (s scoreSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
   205  
   206  // Less compares a to b, and returns true if a is less than b.
   207  func (s scoreSorter) Less(a, b int) bool {
   208  	first := s[a]
   209  	second := s[b]
   210  
   211  	if first.Score > second.Score {
   212  		return false
   213  	}
   214  	if first.Score < second.Score {
   215  		return true
   216  	}
   217  	if first.Name == second.Name {
   218  		v1, err := semver.NewVersion(first.Chart.Version)
   219  		if err != nil {
   220  			return true
   221  		}
   222  		v2, err := semver.NewVersion(second.Chart.Version)
   223  		if err != nil {
   224  			return true
   225  		}
   226  		// Sort so that the newest chart is higher than the oldest chart. This is
   227  		// the opposite of what you'd expect in a function called Less.
   228  		return v1.GreaterThan(v2)
   229  	}
   230  	return first.Name < second.Name
   231  }
   232  
   233  func indstr(name string, ref *repo.ChartVersion) string {
   234  	i := ref.Name + sep + name + "/" + ref.Name + sep +
   235  		ref.Description + sep + strings.Join(ref.Keywords, " ")
   236  	return i
   237  }