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