Skip to content

Commit

Permalink
Add fragments support to related
Browse files Browse the repository at this point in the history
  • Loading branch information
bep committed Feb 11, 2023
1 parent 9af78d1 commit 2c7bb38
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 15 deletions.
9 changes: 9 additions & 0 deletions hugolib/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"go.uber.org/atomic"

"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/related"

"github.com/gohugoio/hugo/markup/converter"

Expand Down Expand Up @@ -148,6 +149,14 @@ func (p *pageState) GetIdentity() identity.Identity {
return identity.NewPathIdentity(files.ComponentFolderContent, filepath.FromSlash(p.Pathc()))
}

func (p *pageState) Fragments() []string {
return []string{"ref1"}
}

func (p *pageState) DocumentFragment(s string) related.Document {
return p
}

func (p *pageState) GitInfo() source.GitInfo {
return p.gitInfo
}
Expand Down
68 changes: 68 additions & 0 deletions related/integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2023 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 related_test

import (
"testing"

"github.com/gohugoio/hugo/hugolib"
)

func TestRelatedHeadings(t *testing.T) {
t.Parallel()

files := `
-- hugo.toml --
[related]
includeNewer = false
threshold = 80
toLower = false
[[related.indices]]
name = 'pagerefs'
type = 'fragment'
weight = 100
-- content/p1.md --
---
title: p1
pagerefs: ['ref1']
---
{{< see-also >}}
-- content/p2.md --
---
title: p2
---
## First title {#ref1}
{{< see-also "ref1" >}}
-- layouts/shortcodes/see-also.html --
{{ $related := site.RegularPages.Related .Page }}
Got: {{ len $related }}
{{ range $i, $e := $related }}
{{ $i }}: {{ $e.RelPermalink }}
{{ end }}
-- layouts/_default/single.html --
{{ .Content }}
`

b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
}).Build()

b.AssertFileContent("public/p1/index.html", "foo")

}
90 changes: 80 additions & 10 deletions related/inverted_index.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ import (
"github.com/mitchellh/mapstructure"
)

const (
TypeBasic = "basic"
TypeFragment = "fragment"
)

var (
_ Keyword = (*StringKeyword)(nil)
zeroDate = time.Time{}
Expand All @@ -37,8 +42,8 @@ var (
DefaultConfig = Config{
Threshold: 80,
Indices: IndexConfigs{
IndexConfig{Name: "keywords", Weight: 100},
IndexConfig{Name: "date", Weight: 10},
IndexConfig{Name: "keywords", Weight: 100, Type: TypeBasic},
IndexConfig{Name: "date", Weight: 10, Type: TypeBasic},
},
}
)
Expand Down Expand Up @@ -84,6 +89,15 @@ func (c *Config) Add(index IndexConfig) {
c.Indices = append(c.Indices, index)
}

func (c *Config) HasType(s string) bool {
for _, i := range c.Indices {
if i.Type == s {
return true
}
}
return false
}

// IndexConfigs holds a set of index configurations.
type IndexConfigs []IndexConfig

Expand All @@ -92,6 +106,9 @@ type IndexConfig struct {
// The index name. This directly maps to a field or Param name.
Name string

// The index type.
Type string

// Contextual pattern used to convert the Param value into a string.
// Currently only used for dates. Can be used to, say, bump posts in the same
// time frame when searching for related documents.
Expand Down Expand Up @@ -120,6 +137,14 @@ type Document interface {
Name() string
}

// FragmentProvider is an optional interface that can be implemented by a Document.
type FragmentProvider interface {
Fragments() []string

// For internal use.
DocumentFragment(s string) Document
}

// InvertedIndex holds an inverted index, also sometimes named posting list, which
// lists, for every possible search term, the documents that contain that term.
type InvertedIndex struct {
Expand Down Expand Up @@ -179,6 +204,14 @@ func (idx *InvertedIndex) Add(docs ...Document) error {
for _, keyword := range words {
setm[keyword] = append(setm[keyword], doc)
}

if config.Type == TypeFragment {
if fp, ok := doc.(FragmentProvider); ok {
for _, fragment := range fp.Fragments() {
setm[FragmentKeyword(fragment)] = append(setm[FragmentKeyword(fragment)], doc)
}
}
}
}
}

Expand Down Expand Up @@ -262,18 +295,28 @@ func (idx *InvertedIndex) SearchDoc(doc Document, indices ...string) ([]Document
return idx.searchDate(doc.PublishDate(), q...)
}

// TODO1 apply to all.
func (cfg IndexConfig) stringToKeyword(s string) Keyword {
// TODO1 apply to all.
if cfg.ToLower {
s = strings.ToLower(s)
}
if cfg.Type == TypeFragment {
return FragmentKeyword(s)
}
return StringKeyword(s)
}

// ToKeywords returns a Keyword slice of the given input.
func (cfg IndexConfig) ToKeywords(v any) ([]Keyword, error) {
var (
keywords []Keyword
toLower = cfg.ToLower
)

switch vv := v.(type) {
case string:
if toLower {
vv = strings.ToLower(vv)
}
keywords = append(keywords, StringKeyword(vv))
keywords = append(keywords, cfg.stringToKeyword(vv))
case []string:
if toLower {
vc := make([]string, len(vv))
Expand All @@ -283,7 +326,7 @@ func (cfg IndexConfig) ToKeywords(v any) ([]Keyword, error) {
}
vv = vc
}
keywords = append(keywords, StringsToKeywords(vv...)...)
keywords = append(keywords, cfg.StringsToKeywords(vv...)...)
case []any:
return cfg.ToKeywords(cast.ToStringSlice(vv))
case time.Time:
Expand Down Expand Up @@ -353,6 +396,8 @@ func (idx *InvertedIndex) searchDate(upperDate time.Time, query ...queryElement)
return []Document{}, fmt.Errorf("index config for %q not found", el.Index)
}

isFragment := config.Type == TypeFragment

for _, kw := range el.Keywords {
if docs, found := setm[kw]; found {
for _, doc := range docs {
Expand All @@ -362,12 +407,30 @@ func (idx *InvertedIndex) searchDate(upperDate time.Time, query ...queryElement)
continue
}
}

if isFragment {

// Pick the first matching fragment.
if ff, ok := doc.(FragmentProvider); ok {

fragments := ff.Fragments()
for _, f := range fragments {
if kw == FragmentKeyword(f) {
doc = ff.DocumentFragment(f)
break
}
}
}
}

r, found := matchm[doc]
if !found {
matchm[doc] = newRank(doc, config.Weight)
r = newRank(doc, config.Weight)
matchm[doc] = r
} else {
r.addWeight(config.Weight)
}

}
}
}
Expand Down Expand Up @@ -444,17 +507,24 @@ func (s StringKeyword) String() string {
return string(s)
}

// FragmentKeyword represents a document fragment.
type FragmentKeyword string

func (f FragmentKeyword) String() string {
return string(f)
}

// Keyword is the interface a keyword in the search index must implement.
type Keyword interface {
String() string
}

// StringsToKeywords converts the given slice of strings to a slice of Keyword.
func StringsToKeywords(s ...string) []Keyword {
func (cfg IndexConfig) StringsToKeywords(s ...string) []Keyword {
kw := make([]Keyword, len(s))

for i := 0; i < len(s); i++ {
kw[i] = StringKeyword(s[i])
kw[i] = cfg.stringToKeyword(s[i])
}

return kw
Expand Down
13 changes: 8 additions & 5 deletions related/inverted_index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ func TestSearch(t *testing.T) {

t.Run("search-tags", func(t *testing.T) {
c := qt.New(t)
m, err := idx.search(newQueryElement("tags", StringsToKeywords("a", "b", "d", "z")...))
var cfg IndexConfig
m, err := idx.search(newQueryElement("tags", cfg.StringsToKeywords("a", "b", "d", "z")...))
c.Assert(err, qt.IsNil)
c.Assert(len(m), qt.Equals, 2)
c.Assert(m[0], qt.Equals, docs[0])
Expand All @@ -131,9 +132,10 @@ func TestSearch(t *testing.T) {

t.Run("search-tags-and-keywords", func(t *testing.T) {
c := qt.New(t)
var cfg IndexConfig
m, err := idx.search(
newQueryElement("tags", StringsToKeywords("a", "b", "z")...),
newQueryElement("keywords", StringsToKeywords("a", "b")...))
newQueryElement("tags", cfg.StringsToKeywords("a", "b", "z")...),
newQueryElement("keywords", cfg.StringsToKeywords("a", "b")...))
c.Assert(err, qt.IsNil)
c.Assert(len(m), qt.Equals, 3)
c.Assert(m[0], qt.Equals, docs[3])
Expand Down Expand Up @@ -283,8 +285,9 @@ func BenchmarkRelatedNewIndex(b *testing.B) {
}

func BenchmarkRelatedMatchesIn(b *testing.B) {
q1 := newQueryElement("tags", StringsToKeywords("keyword2", "keyword5", "keyword32", "asdf")...)
q2 := newQueryElement("keywords", StringsToKeywords("keyword3", "keyword4")...)
var icfg IndexConfig
q1 := newQueryElement("tags", icfg.StringsToKeywords("keyword2", "keyword5", "keyword32", "asdf")...)
q2 := newQueryElement("keywords", icfg.StringsToKeywords("keyword3", "keyword4")...)

docs := make([]*testDoc, 1000)
numkeywords := 20
Expand Down

0 comments on commit 2c7bb38

Please sign in to comment.