github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/resources/page/pages_related.go (about)

     1  // Copyright 2019 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package page
    15  
    16  import (
    17  	"sync"
    18  
    19  	"github.com/gohugoio/hugo/common/types"
    20  	"github.com/gohugoio/hugo/related"
    21  	"github.com/pkg/errors"
    22  	"github.com/spf13/cast"
    23  )
    24  
    25  var (
    26  	// Assert that Pages and PageGroup implements the PageGenealogist interface.
    27  	_ PageGenealogist = (Pages)(nil)
    28  	_ PageGenealogist = PageGroup{}
    29  )
    30  
    31  // A PageGenealogist finds related pages in a page collection. This interface is implemented
    32  // by Pages and PageGroup, which makes it available as `{{ .RegularRelated . }}` etc.
    33  type PageGenealogist interface {
    34  
    35  	// Template example:
    36  	// {{ $related := .RegularPages.Related . }}
    37  	Related(doc related.Document) (Pages, error)
    38  
    39  	// Template example:
    40  	// {{ $related := .RegularPages.RelatedIndices . "tags" "date" }}
    41  	RelatedIndices(doc related.Document, indices ...interface{}) (Pages, error)
    42  
    43  	// Template example:
    44  	// {{ $related := .RegularPages.RelatedTo ( keyVals "tags" "hugo", "rocks")  ( keyVals "date" .Date ) }}
    45  	RelatedTo(args ...types.KeyValues) (Pages, error)
    46  }
    47  
    48  // Related searches all the configured indices with the search keywords from the
    49  // supplied document.
    50  func (p Pages) Related(doc related.Document) (Pages, error) {
    51  	result, err := p.searchDoc(doc)
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  
    56  	if page, ok := doc.(Page); ok {
    57  		return result.removeFirstIfFound(page), nil
    58  	}
    59  
    60  	return result, nil
    61  }
    62  
    63  // RelatedIndices searches the given indices with the search keywords from the
    64  // supplied document.
    65  func (p Pages) RelatedIndices(doc related.Document, indices ...interface{}) (Pages, error) {
    66  	indicesStr, err := cast.ToStringSliceE(indices)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  
    71  	result, err := p.searchDoc(doc, indicesStr...)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	if page, ok := doc.(Page); ok {
    77  		return result.removeFirstIfFound(page), nil
    78  	}
    79  
    80  	return result, nil
    81  }
    82  
    83  // RelatedTo searches the given indices with the corresponding values.
    84  func (p Pages) RelatedTo(args ...types.KeyValues) (Pages, error) {
    85  	if len(p) == 0 {
    86  		return nil, nil
    87  	}
    88  
    89  	return p.search(args...)
    90  }
    91  
    92  func (p Pages) search(args ...types.KeyValues) (Pages, error) {
    93  	return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) {
    94  		return idx.SearchKeyValues(args...)
    95  	})
    96  }
    97  
    98  func (p Pages) searchDoc(doc related.Document, indices ...string) (Pages, error) {
    99  	return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) {
   100  		return idx.SearchDoc(doc, indices...)
   101  	})
   102  }
   103  
   104  func (p Pages) withInvertedIndex(search func(idx *related.InvertedIndex) ([]related.Document, error)) (Pages, error) {
   105  	if len(p) == 0 {
   106  		return nil, nil
   107  	}
   108  
   109  	d, ok := p[0].(InternalDependencies)
   110  	if !ok {
   111  		return nil, errors.Errorf("invalid type %T in related search", p[0])
   112  	}
   113  
   114  	cache := d.GetRelatedDocsHandler()
   115  
   116  	searchIndex, err := cache.getOrCreateIndex(p)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	result, err := search(searchIndex)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	if len(result) > 0 {
   127  		mp := make(Pages, len(result))
   128  		for i, match := range result {
   129  			mp[i] = match.(Page)
   130  		}
   131  		return mp, nil
   132  	}
   133  
   134  	return nil, nil
   135  }
   136  
   137  type cachedPostingList struct {
   138  	p Pages
   139  
   140  	postingList *related.InvertedIndex
   141  }
   142  
   143  type RelatedDocsHandler struct {
   144  	cfg related.Config
   145  
   146  	postingLists []*cachedPostingList
   147  	mu           sync.RWMutex
   148  }
   149  
   150  func NewRelatedDocsHandler(cfg related.Config) *RelatedDocsHandler {
   151  	return &RelatedDocsHandler{cfg: cfg}
   152  }
   153  
   154  func (s *RelatedDocsHandler) Clone() *RelatedDocsHandler {
   155  	return NewRelatedDocsHandler(s.cfg)
   156  }
   157  
   158  // This assumes that a lock has been acquired.
   159  func (s *RelatedDocsHandler) getIndex(p Pages) *related.InvertedIndex {
   160  	for _, ci := range s.postingLists {
   161  		if pagesEqual(p, ci.p) {
   162  			return ci.postingList
   163  		}
   164  	}
   165  	return nil
   166  }
   167  
   168  func (s *RelatedDocsHandler) getOrCreateIndex(p Pages) (*related.InvertedIndex, error) {
   169  	s.mu.RLock()
   170  	cachedIndex := s.getIndex(p)
   171  	if cachedIndex != nil {
   172  		s.mu.RUnlock()
   173  		return cachedIndex, nil
   174  	}
   175  	s.mu.RUnlock()
   176  
   177  	s.mu.Lock()
   178  	defer s.mu.Unlock()
   179  
   180  	if cachedIndex := s.getIndex(p); cachedIndex != nil {
   181  		return cachedIndex, nil
   182  	}
   183  
   184  	searchIndex := related.NewInvertedIndex(s.cfg)
   185  
   186  	for _, page := range p {
   187  		if err := searchIndex.Add(page); err != nil {
   188  			return nil, err
   189  		}
   190  	}
   191  
   192  	s.postingLists = append(s.postingLists, &cachedPostingList{p: p, postingList: searchIndex})
   193  
   194  	return searchIndex, nil
   195  }