Skip to content

Commit

Permalink
Search for folders
Browse files Browse the repository at this point in the history
  • Loading branch information
tfabritius committed Jun 21, 2023
1 parent 647e724 commit 846a529
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 13 deletions.
9 changes: 9 additions & 0 deletions backend/model/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@ type Page struct {
Meta ContentMeta `json:"meta"`
}

func (Page) BleveType() string {
return "page"
}

type Folder struct {
Content []FolderEntry `json:"content"`
Meta ContentMeta `json:"meta"`
}

func (Folder) BleveType() string {
return "folder"
}

type ContentMeta struct {
Title string `json:"title" yaml:"title"`
Tags []string `json:"tags" yaml:"tags"`
Expand Down Expand Up @@ -81,4 +89,5 @@ type SearchHit struct {
Meta ContentMeta `json:"meta"`
Fragments map[string][]string `json:"fragments"`
EffectiveACL *[]AccessRule `json:"-"`
IsFolder bool `json:"isFolder"`
}
72 changes: 65 additions & 7 deletions backend/service/content_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ func (s *ContentService) initializeStorage() error {
}

// Create _index.md with default ACL if it doesn't exist
if !s.IsFolder("/") {
if !s.IsFolder("") {
defaultACL := []model.AccessRule{
{Subject: "all", Operations: []model.AccessOp{model.AccessOpRead, model.AccessOpWrite, model.AccessOpDelete}},
}
if err := s.SaveFolder("/", model.ContentMeta{ACL: &defaultACL}); err != nil {
if err := s.SaveFolder("", model.ContentMeta{ACL: &defaultACL}); err != nil {
return fmt.Errorf("could not create default ACL: %w", err)
}
}
Expand Down Expand Up @@ -89,9 +89,16 @@ func (*ContentService) createIndexMapping() *mapping.IndexMappingImpl {

pageMapping := bleve.NewDocumentMapping()
pageMapping.AddSubDocumentMapping("meta", metaMapping)
pageMapping.AddSubDocumentMapping("url", bleve.NewDocumentDisabledMapping())

folderMapping := bleve.NewDocumentMapping()
folderMapping.AddSubDocumentMapping("meta", metaMapping)
folderMapping.AddSubDocumentMapping("content", bleve.NewDocumentDisabledMapping())

indexMapping := bleve.NewIndexMapping()
indexMapping.TypeField = "BleveType"
indexMapping.AddDocumentMapping("page", pageMapping)
indexMapping.AddDocumentMapping("folder", folderMapping)
return indexMapping
}

Expand All @@ -101,12 +108,21 @@ func (s *ContentService) indexFolder(urlPath string, idx *bleve.Index) error {
return err
}

if urlPath != "" {
// Index the folder itself
if err := (*idx).Index(urlPath, folder); err != nil {
return err
}
}

for _, c := range folder.Content {
if c.IsFolder {
// Recursively index subfolder
if err := s.indexFolder(c.Url, idx); err != nil {
return err
}
} else {
// Index page
page, err := s.ReadPage(c.Url, nil)
if err != nil {
return err
Expand Down Expand Up @@ -136,21 +152,38 @@ func (s *ContentService) Search(q string) ([]model.SearchHit, error) {

ret := []model.SearchHit{}
for _, r := range results.Hits {
page, err := s.ReadPage(r.ID, nil)
if err != nil {
return nil, err
var meta model.ContentMeta
isFolder := false

if s.IsPage(r.ID) {
page, err := s.ReadPage(r.ID, nil)
if err != nil {
return nil, err
}
meta = page.Meta
} else if s.IsFolder(r.ID) {
isFolder = true
folder, err := s.ReadFolder(r.ID)
if err != nil {
return nil, err
}
meta = folder.Meta
} else {
continue
}

metas, err := s.ReadAncestorsMeta(r.ID)
if err != nil {
return nil, err
}
acl := s.GetEffectivePermissions(page.Meta, metas)
acl := s.GetEffectivePermissions(meta, metas)

ret = append(ret, model.SearchHit{
Url: r.ID,
Meta: page.Meta,
Meta: meta,
Fragments: r.Fragments,
EffectiveACL: acl,
IsFolder: isFolder,
})
}

Expand Down Expand Up @@ -278,6 +311,15 @@ func (s *ContentService) CreateFolder(urlPath string, meta model.ContentMeta) er
return err
}

// Update search index
folder := model.Folder{
Content: nil,
Meta: meta,
}
if err := s.index.Index(urlPath, folder); err != nil {
log.Println("[INDEX] Could not add folder "+urlPath+":", err)
}

return nil
}

Expand Down Expand Up @@ -360,6 +402,17 @@ func (s *ContentService) SaveFolder(urlPath string, meta model.ContentMeta) erro
return fmt.Errorf("could not write index file: %w", err)
}

// Update search index
if urlPath != "" {
folder := model.Folder{
Content: nil,
Meta: meta,
}
if err := s.index.Index(urlPath, folder); err != nil {
log.Println("[INDEX] Could not update folder "+urlPath+":", err)
}
}

return nil
}

Expand All @@ -379,6 +432,11 @@ func (s *ContentService) DeleteEmptyFolder(urlPath string) error {
return err
}

// Update search index
if err := s.index.Delete(urlPath); err != nil {
log.Println("[INDEX] Could not delete folder "+urlPath+":", err)
}

return nil
}

Expand Down
46 changes: 41 additions & 5 deletions backend/test/content_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1019,9 +1019,9 @@ func (s *ContentTestSuite) TestSearch() {
q string
nResults int
}{
{"admin", s.adminToken, "page", 5},
{"user", s.userToken, "page", 4},
{"anonymous", nil, "page", 2},
{"admin", s.adminToken, "title", 5},
{"user", s.userToken, "title", 4},
{"anonymous", nil, "title", 2},
}
for _, tc := range tests {
t := s.T()
Expand All @@ -1043,7 +1043,7 @@ func (s *ContentTestSuite) TestSearch() {
r.Equal("Title", hit.Meta.Title)
r.Len(hit.Meta.Tags, 1)
r.Equal("tag", hit.Meta.Tags[0])
r.NotEmpty(hit.Fragments["url"])
r.NotEmpty(hit.Fragments["meta.title"])
}
})
}
Expand All @@ -1054,7 +1054,7 @@ func (s *ContentTestSuite) TestSearch() {
q string
nResults int
}{
{"url", "page", 5},
{"url", "page", 0},
{"content", "content", 5},
{"meta.title", "title", 5},
{"meta.tags", "tag", 5},
Expand Down Expand Up @@ -1093,3 +1093,39 @@ func (s *ContentTestSuite) TestSearch() {
})
}
}

func (s *ContentTestSuite) TestSearchFolder() {
tests := []struct {
name string
q string
nResults int
}{
{"meta.title", "published", 1},
}
for _, tc := range tests {
t := s.T()
t.Run(tc.name, func(t *testing.T) {
r := require.New(t)

res := s.api("POST", "/search?q="+tc.q,
nil,
s.adminToken)
r.Equal(200, res.Code)

body, _ := jsonbody[[]model.SearchHit](res)
r.Len(body, tc.nResults)

for _, hit := range body {
r.Nil(hit.EffectiveACL)
r.Nil(hit.Meta.ACL)
r.NotEmpty(hit.Url)
r.Equal(tc.q, hit.Meta.Title)
r.Len(hit.Meta.Tags, 0)

r.NotEmpty(hit.Fragments["meta.title"])
r.Len(hit.Fragments["meta.title"], 1)
r.Equal("<mark>"+tc.q+"</mark>", hit.Fragments["meta.title"][0])
}
})
}
}
2 changes: 1 addition & 1 deletion frontend/pages/_search.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ watch(q, () => {
<NuxtLink v-slot="{ navigate, href }" custom :to="`/${result.url}`">
<ElLink :underline="false" :href="href" @click="navigate">
<span class="text-xl flex items-center">
<Icon name="ci:file-blank" class="mr-1" />
<Icon :name="result.isFolder ? 'ci:folder' : 'ci:file-blank'" class="mr-1" />
<span v-if="'meta.title' in result.fragments" v-html="result.fragments['meta.title'][0]" />
<span v-else :class="{ 'font-italic': !result.meta.title }">{{ result.meta.title || 'Untitled' }}</span>
</span>
Expand Down
1 change: 1 addition & 0 deletions frontend/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export interface SearchHit {
url: string
meta: ContentMeta
fragments: Record<string, string[]>
isFolder: boolean
}

// corresponding to service/user_service.go
Expand Down

0 comments on commit 846a529

Please sign in to comment.