forked from gohugoio/hugo
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
hugolib: Implement "related content"
This closes #98, even if this commit does not do full content text search. We may revisit that problem in the future, but that deserves its own issue. Fixes #98
- Loading branch information
Showing
14 changed files
with
1,133 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// Copyright 2017-present The Hugo Authors. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
// Package types contains types shared between packages in Hugo. | ||
package types | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/spf13/cast" | ||
) | ||
|
||
// KeyValues holds an key and a slice of values. | ||
type KeyValues struct { | ||
Key interface{} | ||
Values []interface{} | ||
} | ||
|
||
// KeyString returns the key as a string, an empty string if conversion fails. | ||
func (k KeyValues) KeyString() string { | ||
return cast.ToString(k.Key) | ||
} | ||
|
||
func (k KeyValues) String() string { | ||
return fmt.Sprintf("%v: %v", k.Key, k.Values) | ||
} | ||
|
||
func NewKeyValuesStrings(key string, values ...string) KeyValues { | ||
iv := make([]interface{}, len(values)) | ||
for i := 0; i < len(values); i++ { | ||
iv[i] = values[i] | ||
} | ||
return KeyValues{Key: key, Values: iv} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Copyright 2017-present The Hugo Authors. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package types | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestKeyValues(t *testing.T) { | ||
assert := require.New(t) | ||
|
||
kv := NewKeyValuesStrings("key", "a1", "a2") | ||
|
||
assert.Equal("key", kv.KeyString()) | ||
assert.Equal([]interface{}{"a1", "a2"}, kv.Values) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
// Copyright 2017-present The Hugo Authors. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package hugolib | ||
|
||
import ( | ||
"sync" | ||
|
||
"github.com/gohugoio/hugo/common/types" | ||
"github.com/gohugoio/hugo/related" | ||
"github.com/spf13/cast" | ||
) | ||
|
||
var ( | ||
// Assert that Pages and PageGroup implements the PageGenealogist interface. | ||
_ PageGenealogist = (Pages)(nil) | ||
_ PageGenealogist = PageGroup{} | ||
) | ||
|
||
// A PageGenealogist finds related pages in a page collection. This interface is implemented | ||
// by Pages and PageGroup, which makes it available as `{{ .RegularPages.Related . }}` etc. | ||
type PageGenealogist interface { | ||
|
||
// Template example: | ||
// {{ $related := .RegularPages.Related . }} | ||
Related(doc related.Document) (Pages, error) | ||
|
||
// Template example: | ||
// {{ $related := .RegularPages.RelatedIndices . "tags" "date" }} | ||
RelatedIndices(doc related.Document, indices ...interface{}) (Pages, error) | ||
|
||
// Template example: | ||
// {{ $related := .RegularPages.RelatedTo ( keyVals "tags" "hugo", "rocks") ( keyVals "date" .Date ) }} | ||
RelatedTo(args ...types.KeyValues) (Pages, error) | ||
} | ||
|
||
// Related searches all the configured indices with the search keywords from the | ||
// supplied document. | ||
func (p Pages) Related(doc related.Document) (Pages, error) { | ||
page, err := unwrapPage(doc) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
result, err := p.searchDoc(page) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return result.removeFirstIfFound(page), nil | ||
} | ||
|
||
// RelatedIndices searches the given indices with the search keywords from the | ||
// supplied document. | ||
func (p Pages) RelatedIndices(doc related.Document, indices ...interface{}) (Pages, error) { | ||
page, err := unwrapPage(doc) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
indicesStr, err := cast.ToStringSliceE(indices) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
result, err := p.searchDoc(page, indicesStr...) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return result.removeFirstIfFound(page), nil | ||
|
||
} | ||
|
||
// RelatedTo searches the given indices with the corresponding values. | ||
func (p Pages) RelatedTo(args ...types.KeyValues) (Pages, error) { | ||
if len(p) == 0 { | ||
return nil, nil | ||
} | ||
|
||
return p.search(args...) | ||
|
||
} | ||
|
||
func (p Pages) search(args ...types.KeyValues) (Pages, error) { | ||
return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) { | ||
return idx.SearchKeyValues(args...) | ||
}) | ||
|
||
} | ||
|
||
func (p Pages) searchDoc(doc related.Document, indices ...string) (Pages, error) { | ||
return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) { | ||
return idx.SearchDoc(doc, indices...) | ||
}) | ||
} | ||
|
||
func (p Pages) withInvertedIndex(search func(idx *related.InvertedIndex) ([]related.Document, error)) (Pages, error) { | ||
if len(p) == 0 { | ||
return nil, nil | ||
} | ||
|
||
cache := p[0].s.relatedDocsHandler | ||
|
||
searchIndex, err := cache.getOrCreateIndex(p) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
result, err := search(searchIndex) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if len(result) > 0 { | ||
mp := make(Pages, len(result)) | ||
for i, match := range result { | ||
mp[i] = match.(*Page) | ||
} | ||
return mp, nil | ||
} | ||
|
||
return nil, nil | ||
} | ||
|
||
type cachedPostingList struct { | ||
p Pages | ||
|
||
postingList *related.InvertedIndex | ||
} | ||
|
||
type relatedDocsHandler struct { | ||
// This is configured in site or langugage config. | ||
cfg related.Config | ||
|
||
postingLists []*cachedPostingList | ||
mu sync.RWMutex | ||
} | ||
|
||
func newSearchIndexHandler(cfg related.Config) *relatedDocsHandler { | ||
return &relatedDocsHandler{cfg: cfg} | ||
} | ||
|
||
// This assumes that a lock has been aquired. | ||
func (s *relatedDocsHandler) getIndex(p Pages) *related.InvertedIndex { | ||
for _, ci := range s.postingLists { | ||
if fastEqualPages(p, ci.p) { | ||
return ci.postingList | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (s *relatedDocsHandler) getOrCreateIndex(p Pages) (*related.InvertedIndex, error) { | ||
s.mu.RLock() | ||
cachedIndex := s.getIndex(p) | ||
if cachedIndex != nil { | ||
s.mu.RUnlock() | ||
return cachedIndex, nil | ||
} | ||
s.mu.RUnlock() | ||
|
||
s.mu.Lock() | ||
defer s.mu.Unlock() | ||
|
||
if cachedIndex := s.getIndex(p); cachedIndex != nil { | ||
return cachedIndex, nil | ||
} | ||
|
||
searchIndex := related.NewInvertedIndex(s.cfg) | ||
|
||
for _, page := range p { | ||
searchIndex.Add(page) | ||
} | ||
|
||
s.postingLists = append(s.postingLists, &cachedPostingList{p: p, postingList: searchIndex}) | ||
|
||
return searchIndex, nil | ||
} |
Oops, something went wrong.