pages_related.go (4899B)
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 "fmt"
18 "sync"
19
20 "github.com/gohugoio/hugo/common/types"
21 "github.com/gohugoio/hugo/related"
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 ...any) (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 ...any) (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, fmt.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 }