From 987191f9b0860844555ae3756c63a12fbdaa3fa9 Mon Sep 17 00:00:00 2001 From: Sergio Vera Date: Mon, 9 Dec 2024 13:38:34 +0100 Subject: [PATCH] Authors controller (#112) * Added authors controller * WIP * Fixed link to related docs * Use ellipsis in cover large texts, cleaned up API * Added missing tranlations * Updated tests * Cleanup * Further cleanup * Added test --- internal/index/author.go | 13 ++ internal/index/bleve.go | 23 +- internal/index/bleve_read.go | 220 ++++++++++-------- internal/index/bleve_test.go | 27 +++ internal/index/bleve_write.go | 62 +++-- internal/index/document.go | 25 +- internal/metadata/epub.go | 6 +- internal/metadata/metadata.go | 2 +- internal/metadata/pdf.go | 2 +- internal/webserver/controller.go | 7 + .../webserver/controller/author/controller.go | 40 ++++ .../webserver/controller/author/search.go | 68 ++++++ .../controller/document/controller.go | 2 +- .../webserver/controller/document/detail.go | 5 +- .../webserver/controller/home/controller.go | 2 +- internal/webserver/controller/home/index.go | 3 +- internal/webserver/controller/user/create.go | 2 +- internal/webserver/controller/user/edit.go | 2 +- internal/webserver/controller/user/list.go | 2 +- internal/webserver/controller/user/new.go | 2 +- internal/webserver/controller/user/update.go | 8 +- .../webserver/embedded/translations/es.yml | 1 + .../webserver/embedded/translations/fr.yml | 1 + .../embedded/views/author/results.html | 17 ++ .../webserver/embedded/views/document.html | 18 +- .../embedded/views/partials/actions.html | 4 +- .../embedded/views/partials/cover.html | 4 +- .../embedded/views/partials/docs-list.html | 6 +- .../embedded/views/{users => user}/edit.html | 0 .../embedded/views/{users => user}/index.html | 0 .../embedded/views/{users => user}/new.html | 0 internal/webserver/routes.go | 1 + internal/webserver/search_test.go | 1 + 33 files changed, 410 insertions(+), 166 deletions(-) create mode 100644 internal/index/author.go create mode 100644 internal/webserver/controller/author/controller.go create mode 100644 internal/webserver/controller/author/search.go create mode 100644 internal/webserver/embedded/views/author/results.html rename internal/webserver/embedded/views/{users => user}/edit.html (100%) rename internal/webserver/embedded/views/{users => user}/index.html (100%) rename internal/webserver/embedded/views/{users => user}/new.html (100%) diff --git a/internal/index/author.go b/internal/index/author.go new file mode 100644 index 0000000..4748235 --- /dev/null +++ b/internal/index/author.go @@ -0,0 +1,13 @@ +package index + +type Author struct { + Slug string + Name string + Type string +} + +// BleveType is part of the bleve.Classifier interface and its purpose is to tell the indexer +// the type of the document, which will be used to decide which analyzer will parse it. +func (a Author) BleveType() string { + return "author" +} diff --git a/internal/index/bleve.go b/internal/index/bleve.go index 48459f9..4d034dd 100644 --- a/internal/index/bleve.go +++ b/internal/index/bleve.go @@ -24,7 +24,12 @@ import ( // Version identifies the mapping used for indexing. Any changes in the mapping requires an increase // of version, to signal that a new index needs to be created. -const Version = "v3" +const Version = "v4" + +const ( + TypeDocument = "document" + TypeAuthor = "author" +) // Metadata fields var ( @@ -111,29 +116,29 @@ func CreateMapping() mapping.IndexMapping { indexMapping.AddDocumentMapping(lang, bleve.NewDocumentMapping()) indexMapping.TypeMapping[lang].DefaultAnalyzer = lang + indexMapping.TypeMapping[lang].AddFieldMappingsAt("Slug", keywordFieldMapping) indexMapping.TypeMapping[lang].AddFieldMappingsAt("Title", noStopWordsTextFieldMapping) indexMapping.TypeMapping[lang].AddFieldMappingsAt("Authors", simpleTextFieldMapping) + indexMapping.TypeMapping[lang].AddFieldMappingsAt("AuthorsSlugs", keywordFieldMapping) indexMapping.TypeMapping[lang].AddFieldMappingsAt("Description", textFieldMapping) indexMapping.TypeMapping[lang].AddFieldMappingsAt("Subjects", textFieldMapping) + indexMapping.TypeMapping[lang].AddFieldMappingsAt("SubjectsSlugs", keywordFieldMapping) indexMapping.TypeMapping[lang].AddFieldMappingsAt("Series", noStopWordsTextFieldMapping) - indexMapping.TypeMapping[lang].AddFieldMappingsAt("Slug", keywordFieldMapping) - indexMapping.TypeMapping[lang].AddFieldMappingsAt("SeriesEq", keywordFieldMapping) - indexMapping.TypeMapping[lang].AddFieldMappingsAt("AuthorsEq", keywordFieldMapping) - indexMapping.TypeMapping[lang].AddFieldMappingsAt("SubjectsEq", keywordFieldMapping) + indexMapping.TypeMapping[lang].AddFieldMappingsAt("SeriesSlug", keywordFieldMapping) indexMapping.TypeMapping[lang].AddFieldMappingsAt("Language", keywordFieldMappingNotIndexable) indexMapping.TypeMapping[lang].AddFieldMappingsAt("Year", keywordFieldMappingNotIndexable) } indexMapping.DefaultMapping.DefaultAnalyzer = defaultAnalyzer + indexMapping.DefaultMapping.AddFieldMappingsAt("Slug", keywordFieldMapping) indexMapping.DefaultMapping.AddFieldMappingsAt("Title", simpleTextFieldMapping) indexMapping.DefaultMapping.AddFieldMappingsAt("Authors", simpleTextFieldMapping) + indexMapping.DefaultMapping.AddFieldMappingsAt("AuthorsSlugs", keywordFieldMapping) indexMapping.DefaultMapping.AddFieldMappingsAt("Description", simpleTextFieldMapping) indexMapping.DefaultMapping.AddFieldMappingsAt("Subjects", simpleTextFieldMapping) + indexMapping.DefaultMapping.AddFieldMappingsAt("SubjectsSlugs", keywordFieldMapping) indexMapping.DefaultMapping.AddFieldMappingsAt("Series", simpleTextFieldMapping) - indexMapping.DefaultMapping.AddFieldMappingsAt("Slug", keywordFieldMapping) - indexMapping.DefaultMapping.AddFieldMappingsAt("SeriesEq", keywordFieldMapping) - indexMapping.DefaultMapping.AddFieldMappingsAt("AuthorsEq", keywordFieldMapping) - indexMapping.DefaultMapping.AddFieldMappingsAt("SubjectsEq", keywordFieldMapping) + indexMapping.DefaultMapping.AddFieldMappingsAt("SeriesSlug", keywordFieldMapping) indexMapping.DefaultMapping.AddFieldMappingsAt("Language", keywordFieldMappingNotIndexable) indexMapping.DefaultMapping.AddFieldMappingsAt("Year", keywordFieldMappingNotIndexable) diff --git a/internal/index/bleve_read.go b/internal/index/bleve_read.go index 01f9bed..326a5b0 100644 --- a/internal/index/bleve_read.go +++ b/internal/index/bleve_read.go @@ -10,13 +10,15 @@ import ( "time" "github.com/blevesearch/bleve/v2" + "github.com/blevesearch/bleve/v2/search" "github.com/blevesearch/bleve/v2/search/query" - "github.com/gosimple/slug" "github.com/spf13/afero" "github.com/svera/coreander/v4/internal/metadata" "github.com/svera/coreander/v4/internal/result" ) +var docSearchFields = []string{"ID", "Title", "Slug", "Authors", "AuthorsSlugs", "Description", "Year", "Words", "Series", "SeriesSlug", "SeriesIndex", "Pages", "Format", "Subjects", "SubjectsSlugs"} + func (b *BleveIndexer) IndexingProgress() (Progress, error) { var progress Progress @@ -59,7 +61,7 @@ func (b *BleveIndexer) Search(keywords string, page, resultsPerPage int) (result } } - for _, prefix := range []string{"AuthorsEq:", "SeriesEq:", "SubjectsEq:"} { + for _, prefix := range []string{"AuthorsSlugs:", "SeriesSlug:", "SubjectsSlugs:"} { unescaped, err := url.QueryUnescape(strings.TrimSpace(keywords)) if err != nil { break @@ -71,7 +73,6 @@ func (b *BleveIndexer) Search(keywords string, page, resultsPerPage int) (result terms := strings.Split(unescaped, ",") qb := bleve.NewDisjunctionQuery() for _, term := range terms { - term = strings.ReplaceAll(slug.Make(term), "-", "") qs := bleve.NewTermQuery(term) qs.SetField(strings.TrimSuffix(prefix, ":")) qb.AddQuery(qs) @@ -146,7 +147,7 @@ func (b *BleveIndexer) runPaginatedQuery(query query.Query, page, resultsPerPage searchOptions := bleve.NewSearchRequestOptions(query, resultsPerPage, (page-1)*resultsPerPage, false) searchOptions.SortBy([]string{"-_score", "Series", "SeriesIndex"}) - searchOptions.Fields = []string{"ID", "Slug", "Title", "Authors", "Description", "Year", "Words", "Series", "SeriesIndex", "Pages", "Type", "Subjects"} + searchOptions.Fields = docSearchFields searchResult, err := b.idx.Search(searchOptions) if err != nil { return result.Paginated[[]Document]{}, err @@ -156,26 +157,10 @@ func (b *BleveIndexer) runPaginatedQuery(query query.Query, page, resultsPerPage return res, nil } - docs := make([]Document, 0, len(searchResult.Hits)) - - for _, val := range searchResult.Hits { - doc := Document{ - ID: val.ID, - Slug: val.Fields["Slug"].(string), - Metadata: metadata.Metadata{ - Title: val.Fields["Title"].(string), - Authors: slicer(val.Fields["Authors"]), - Description: template.HTML(val.Fields["Description"].(string)), - Year: val.Fields["Year"].(string), - Words: val.Fields["Words"].(float64), - Series: val.Fields["Series"].(string), - SeriesIndex: val.Fields["SeriesIndex"].(float64), - Pages: int(val.Fields["Pages"].(float64)), - Type: val.Fields["Type"].(string), - Subjects: slicer(val.Fields["Subjects"]), - }, - } - docs = append(docs, doc) + docs := make([]Document, len(searchResult.Hits)) + + for i, val := range searchResult.Hits { + docs[i] = hydrateDocument(val) } return result.NewPaginated[[]Document]( @@ -187,15 +172,28 @@ func (b *BleveIndexer) runPaginatedQuery(query query.Query, page, resultsPerPage } // Count returns the number of indexed documents -func (b *BleveIndexer) Count() (uint64, error) { - return b.idx.DocCount() +func (b *BleveIndexer) Count(t string) (uint64, error) { + tq := bleve.NewTermQuery(t) + tq.SetField("Type") + + searchRequest := bleve.NewSearchRequest(tq) + searchResult, err := b.idx.Search(searchRequest) + if err != nil { + return 0, err + } + return searchResult.Total, nil } func (b *BleveIndexer) Document(slug string) (Document, error) { + compoundQuery := bleve.NewConjunctionQuery() query := bleve.NewTermQuery(slug) query.SetField("Slug") - searchOptions := bleve.NewSearchRequest(query) - searchOptions.Fields = []string{"ID", "Slug", "Title", "Authors", "Description", "Year", "Words", "Series", "SeriesIndex", "Pages", "Type", "Subjects"} + typeQuery := bleve.NewTermQuery(TypeDocument) + typeQuery.SetField("Type") + compoundQuery.AddQuery(query, typeQuery) + + searchOptions := bleve.NewSearchRequest(compoundQuery) + searchOptions.Fields = docSearchFields searchResult, err := b.idx.Search(searchOptions) if err != nil { return Document{}, err @@ -204,52 +202,26 @@ func (b *BleveIndexer) Document(slug string) (Document, error) { return Document{}, fmt.Errorf("Document with slug %s not found", slug) } - return Document{ - ID: searchResult.Hits[0].ID, - Slug: searchResult.Hits[0].Fields["Slug"].(string), - Metadata: metadata.Metadata{ - Title: searchResult.Hits[0].Fields["Title"].(string), - Authors: slicer(searchResult.Hits[0].Fields["Authors"]), - Description: template.HTML(searchResult.Hits[0].Fields["Description"].(string)), - Year: searchResult.Hits[0].Fields["Year"].(string), - Words: searchResult.Hits[0].Fields["Words"].(float64), - Series: searchResult.Hits[0].Fields["Series"].(string), - SeriesIndex: searchResult.Hits[0].Fields["SeriesIndex"].(float64), - Pages: int(searchResult.Hits[0].Fields["Pages"].(float64)), - Type: searchResult.Hits[0].Fields["Type"].(string), - Subjects: slicer(searchResult.Hits[0].Fields["Subjects"]), - }, - }, nil + return hydrateDocument(searchResult.Hits[0]), nil } func (b *BleveIndexer) Documents(IDs []string) (map[string]Document, error) { + compoundQuery := bleve.NewConjunctionQuery() docs := make(map[string]Document, len(IDs)) query := bleve.NewDocIDQuery(IDs) - searchOptions := bleve.NewSearchRequest(query) - searchOptions.Fields = []string{"ID", "Slug", "Title", "Authors", "Description", "Year", "Words", "Series", "SeriesIndex", "Pages", "Type", "Subjects"} + typeQuery := bleve.NewTermQuery(TypeDocument) + typeQuery.SetField("Type") + compoundQuery.AddQuery(query, typeQuery) + + searchOptions := bleve.NewSearchRequest(compoundQuery) + searchOptions.Fields = docSearchFields searchResult, err := b.idx.Search(searchOptions) if err != nil { return docs, err } for _, hit := range searchResult.Hits { - docs[hit.ID] = - Document{ - ID: hit.ID, - Slug: hit.Fields["Slug"].(string), - Metadata: metadata.Metadata{ - Title: hit.Fields["Title"].(string), - Authors: slicer(hit.Fields["Authors"]), - Description: template.HTML(hit.Fields["Description"].(string)), - Year: hit.Fields["Year"].(string), - Words: hit.Fields["Words"].(float64), - Series: hit.Fields["Series"].(string), - SeriesIndex: hit.Fields["SeriesIndex"].(float64), - Pages: int(hit.Fields["Pages"].(float64)), - Type: hit.Fields["Type"].(string), - Subjects: slicer(hit.Fields["Subjects"]), - }, - } + docs[hit.ID] = hydrateDocument(hit) } return docs, nil @@ -266,17 +238,15 @@ func (b *BleveIndexer) SameSubjects(slugID string, quantity int) ([]Document, er bq := bleve.NewBooleanQuery() subjectsCompoundQuery := bleve.NewDisjunctionQuery() - for _, subject := range doc.Subjects { - subject = strings.ReplaceAll(slug.Make(subject), "-", "") - qu := bleve.NewTermQuery(subject) - qu.SetField("SubjectsEq") + for _, slug := range doc.SubjectsSlugs { + qu := bleve.NewTermQuery(slug) + qu.SetField("SubjectsSlugs") subjectsCompoundQuery.AddQuery(qu) } - if doc.Series != "" { - series := strings.ReplaceAll(slug.Make(doc.Series), "-", "") - sq := bleve.NewTermQuery(series) - sq.SetField("SeriesEq") + if doc.SeriesSlug != "" { + sq := bleve.NewTermQuery(doc.SeriesSlug) + sq.SetField("SeriesSlug") bq.AddMustNot(sq) } @@ -284,15 +254,18 @@ func (b *BleveIndexer) SameSubjects(slugID string, quantity int) ([]Document, er bq.AddMustNot(bleve.NewDocIDQuery([]string{doc.ID})) authorsCompoundQuery := bleve.NewDisjunctionQuery() - for _, author := range doc.Authors { - author = strings.ReplaceAll(slug.Make(author), "-", "") - qa := bleve.NewTermQuery(author) - qa.SetField("AuthorsEq") + for _, slug := range doc.AuthorsSlugs { + qa := bleve.NewTermQuery(slug) + qa.SetField("AuthorsSlugs") authorsCompoundQuery.AddQuery(qa) } bq.AddMustNot(authorsCompoundQuery) - res := make([]Document, 0, quantity) + typeQuery := bleve.NewTermQuery(TypeDocument) + typeQuery.SetField("Type") + bq.AddMust(typeQuery) + + res := make([]Document, quantity) for i := 0; i < quantity; i++ { doc, err := b.runQuery(bq, 1) if err != nil { @@ -301,11 +274,10 @@ func (b *BleveIndexer) SameSubjects(slugID string, quantity int) ([]Document, er if len(doc) == 0 { return res, nil } - res = append(res, doc[0]) - for _, author := range doc[0].Authors { - author = strings.ReplaceAll(slug.Make(author), "-", "") - qa := bleve.NewTermQuery(author) - qa.SetField("AuthorsEq") + res[i] = doc[0] + for _, slug := range doc[0].AuthorsSlugs { + qa := bleve.NewTermQuery(slug) + qa.SetField("AuthorsSlugs") authorsCompoundQuery.AddQuery(qa) } bq.AddMustNot(authorsCompoundQuery) @@ -323,10 +295,9 @@ func (b *BleveIndexer) SameAuthors(slugID string, quantity int) ([]Document, err } authorsCompoundQuery := bleve.NewDisjunctionQuery() - for _, author := range doc.Authors { - author = strings.ReplaceAll(slug.Make(author), "-", "") - qu := bleve.NewTermQuery(author) - qu.SetField("AuthorsEq") + for _, slug := range doc.AuthorsSlugs { + qu := bleve.NewTermQuery(slug) + qu.SetField("AuthorsSlugs") authorsCompoundQuery.AddQuery(qu) } bq := bleve.NewBooleanQuery() @@ -334,12 +305,15 @@ func (b *BleveIndexer) SameAuthors(slugID string, quantity int) ([]Document, err bq.AddMustNot(bleve.NewDocIDQuery([]string{doc.ID})) if doc.Series != "" { - series := strings.ReplaceAll(slug.Make(doc.Series), "-", "") - sq := bleve.NewTermQuery(series) - sq.SetField("SeriesEq") + sq := bleve.NewTermQuery(doc.SeriesSlug) + sq.SetField("SeriesSlug") bq.AddMustNot(sq) } + typeQuery := bleve.NewTermQuery(TypeDocument) + typeQuery.SetField("Type") + bq.AddMust(typeQuery) + return b.runQuery(bq, quantity) } @@ -350,14 +324,21 @@ func (b *BleveIndexer) SameSeries(slugID string, quantity int) ([]Document, erro return []Document{}, err } + if doc.Series == "" { + return []Document{}, err + } + bq := bleve.NewBooleanQuery() bq.AddMustNot(bleve.NewDocIDQuery([]string{doc.ID})) - series := strings.ReplaceAll(slug.Make(doc.Series), "-", "") - sq := bleve.NewMatchPhraseQuery(series) - sq.SetField("SeriesEq") + sq := bleve.NewTermQuery(doc.SeriesSlug) + sq.SetField("SeriesSlug") bq.AddMust(sq) + typeQuery := bleve.NewTermQuery(TypeDocument) + typeQuery.SetField("Type") + bq.AddMust(typeQuery) + return b.runQuery(bq, quantity) } @@ -393,3 +374,60 @@ func slicer(val interface{}) []string { return termsStrings } + +func (b *BleveIndexer) SearchByAuthor(authorSlug string, page, resultsPerPage int) (result.Paginated[[]Document], error) { + aq := bleve.NewTermQuery(authorSlug) + aq.SetField("AuthorsSlugs") + + return b.runPaginatedQuery(aq, page, resultsPerPage) +} + +func (b *BleveIndexer) Author(slug string) (Author, error) { + authorsCompoundQuery := bleve.NewConjunctionQuery() + + aq := bleve.NewTermQuery(slug) + aq.SetField("Slug") + authorsCompoundQuery.AddQuery(aq) + + tq := bleve.NewTermQuery(TypeAuthor) + tq.SetField("Type") + authorsCompoundQuery.AddQuery(tq) + + searchOptions := bleve.NewSearchRequest(authorsCompoundQuery) + searchOptions.Fields = []string{"Name"} + searchResult, err := b.idx.Search(searchOptions) + if err != nil { + return Author{}, err + } + if searchResult.Total == 0 { + return Author{}, fmt.Errorf("Author with slug %s not found", slug) + } + + return Author{ + Name: searchResult.Hits[0].Fields["Name"].(string), + }, nil +} + +func hydrateDocument(match *search.DocumentMatch) Document { + doc := Document{ + ID: match.ID, + Metadata: metadata.Metadata{ + Title: match.Fields["Title"].(string), + Authors: slicer(match.Fields["Authors"]), + Description: template.HTML(match.Fields["Description"].(string)), + Year: match.Fields["Year"].(string), + Words: match.Fields["Words"].(float64), + Series: match.Fields["Series"].(string), + SeriesIndex: match.Fields["SeriesIndex"].(float64), + Pages: int(match.Fields["Pages"].(float64)), + Subjects: slicer(match.Fields["Subjects"]), + Format: match.Fields["Format"].(string), + }, + Slug: match.Fields["Slug"].(string), + AuthorsSlugs: slicer(match.Fields["AuthorsSlugs"]), + SeriesSlug: match.Fields["SeriesSlug"].(string), + SubjectsSlugs: slicer(match.Fields["SubjectsSlugs"]), + } + + return doc +} diff --git a/internal/index/bleve_test.go b/internal/index/bleve_test.go index eedc531..8b87f26 100644 --- a/internal/index/bleve_test.go +++ b/internal/index/bleve_test.go @@ -85,6 +85,9 @@ func testCases() []testCase { Description: "Just test metadata", Subjects: []string{"History", "Middle age"}, }, + AuthorsSlugs: []string{"perez"}, + SeriesSlug: "", + SubjectsSlugs: []string{"history", "middle-age"}, }, }, ), @@ -114,6 +117,9 @@ func testCases() []testCase { Description: "Just test metadata", Subjects: []string{""}, }, + AuthorsSlugs: []string{"benoit"}, + SeriesSlug: "", + SubjectsSlugs: []string{""}, }, }, ), @@ -143,6 +149,9 @@ func testCases() []testCase { Description: "Just test metadata", Subjects: []string{""}, }, + AuthorsSlugs: []string{"clifford-d-simak"}, + SeriesSlug: "", + SubjectsSlugs: []string{""}, }, }, ), @@ -171,6 +180,9 @@ func testCases() []testCase { Description: "Just test metadata", Subjects: []string{""}, }, + AuthorsSlugs: []string{"james-ellroy"}, + SeriesSlug: "", + SubjectsSlugs: []string{""}, }, }, ), @@ -199,6 +211,9 @@ func testCases() []testCase { Description: "Just test metadata", Subjects: []string{""}, }, + AuthorsSlugs: []string{"james-ellroy"}, + SeriesSlug: "", + SubjectsSlugs: []string{""}, }, }, ), @@ -228,6 +243,9 @@ func testCases() []testCase { Description: "Just test metadata", Subjects: []string{"History", "Middle age"}, }, + AuthorsSlugs: []string{"anonimo"}, + SeriesSlug: "", + SubjectsSlugs: []string{"history", "middle-age"}, }, }, ), @@ -257,6 +275,9 @@ func testCases() []testCase { Description: "Just test metadata", Subjects: []string{"History", "Middle age"}, }, + AuthorsSlugs: []string{"anonimo"}, + SeriesSlug: "", + SubjectsSlugs: []string{"history", "middle-age"}, }, }, ), @@ -286,6 +307,9 @@ func testCases() []testCase { Description: "Just test metadata", Subjects: []string{"History", "Middle age"}, }, + AuthorsSlugs: []string{"irene-vallejo"}, + SeriesSlug: "", + SubjectsSlugs: []string{"history", "middle-age"}, }, }, ), @@ -315,6 +339,9 @@ func testCases() []testCase { Description: "Just test metadata", Subjects: []string{"History", "WWII"}, }, + AuthorsSlugs: []string{"patrick-r-reid"}, + SeriesSlug: "", + SubjectsSlugs: []string{"history", "wwii"}, }, }, ), diff --git a/internal/index/bleve_write.go b/internal/index/bleve_write.go index fb848b9..901ba07 100644 --- a/internal/index/bleve_write.go +++ b/internal/index/bleve_write.go @@ -29,6 +29,8 @@ func (b *BleveIndexer) AddFile(file string) (string, error) { document := b.createDocument(meta, file, nil) + indexAuthors(document, b.idx.Index) + err = b.idx.Index(document.ID, document) if err != nil { return "", fmt.Errorf("error indexing file %s: %s", file, err) @@ -73,6 +75,8 @@ func (b *BleveIndexer) AddLibrary(batchSize int, forceIndexing bool) error { batchSlugs[document.Slug] = struct{}{} languages = addLanguage(meta.Language, languages) + indexAuthors(document, batch.Index) + err = batch.Index(document.ID, document) if err != nil { log.Printf("Error indexing file %s: %s\n", fullPath, err) @@ -96,6 +100,20 @@ func (b *BleveIndexer) AddLibrary(batchSize int, forceIndexing bool) error { return e } +func indexAuthors(document Document, index func(id string, data interface{}) error) { + for i, name := range document.Authors { + author := Author{ + Name: name, + Slug: document.AuthorsSlugs[i], + Type: TypeAuthor, + } + + if err := index(author.Slug, author); err != nil { + log.Printf("Error indexing author %s: %s\n", name, err) + } + } +} + func (b *BleveIndexer) isAlreadyIndexed(fullPath string) (bool, string) { doc, err := b.idx.Document(b.id(fullPath)) if err != nil { @@ -134,25 +152,25 @@ func addLanguage(lang string, languages []string) []string { return languages } -func (b *BleveIndexer) createDocument(meta metadata.Metadata, fullPath string, batchSlugs map[string]struct{}) DocumentWrite { - document := DocumentWrite{ - Document: Document{ - Metadata: meta, - }, - SeriesEq: strings.ReplaceAll(slug.Make(meta.Series), "-", ""), - AuthorsEq: make([]string, len(meta.Authors)), - SubjectsEq: make([]string, len(meta.Subjects)), +func (b *BleveIndexer) createDocument(meta metadata.Metadata, fullPath string, batchSlugs map[string]struct{}) Document { + document := Document{ + ID: b.id(fullPath), + Metadata: meta, + Slug: slug.Make(meta.Title), + AuthorsSlugs: make([]string, len(meta.Authors)), + SeriesSlug: slug.Make(meta.Series), + SubjectsSlugs: make([]string, len(meta.Subjects)), + Type: TypeDocument, } - document.ID = b.id(fullPath) document.Slug = b.Slug(document, batchSlugs) - copy(document.AuthorsEq, meta.Authors) - for i := range document.AuthorsEq { - document.AuthorsEq[i] = strings.ReplaceAll(slug.Make(document.AuthorsEq[i]), "-", "") + + for i, author := range meta.Authors { + document.AuthorsSlugs[i] = slug.Make(author) } - copy(document.SubjectsEq, meta.Subjects) - for i := range document.SubjectsEq { - document.SubjectsEq[i] = strings.ReplaceAll(slug.Make(document.SubjectsEq[i]), "-", "") + + for i, subject := range meta.Subjects { + document.SubjectsSlugs[i] = slug.Make(subject) } return document @@ -160,8 +178,8 @@ func (b *BleveIndexer) createDocument(meta metadata.Metadata, fullPath string, b // As Bleve index is not updated until the batch is executed, we need to store the slugs // processed in the current batch in memory to also compare the current doc slug against them. -func (b *BleveIndexer) Slug(document DocumentWrite, batchSlugs map[string]struct{}) string { - docSlug := makeSlug(document) +func (b *BleveIndexer) Slug(document Document, batchSlugs map[string]struct{}) string { + docSlug := makeDocumentSlug(document) exp, err := regexp.Compile(`^[a-zA-Z0-9\-]+(--)[0-9]+$`) if err != nil { log.Fatal(err) @@ -193,11 +211,11 @@ func (b *BleveIndexer) id(file string) string { return strings.TrimPrefix(ID, string(filepath.Separator)) } -func makeSlug(meta DocumentWrite) string { - docSlug := meta.Title - if len(meta.Authors) > 0 { - docSlug = strings.Join(meta.Authors, ", ") + "-" + docSlug +func makeDocumentSlug(doc Document) string { + docSlug := doc.Title + if len(doc.Authors) > 0 { + docSlug = strings.Join(append(doc.Authors, docSlug), "-") } - return slug.MakeLang(docSlug, meta.Language) + return slug.MakeLang(docSlug, doc.Language) } diff --git a/internal/index/document.go b/internal/index/document.go index d5e0af6..4b9a1a0 100644 --- a/internal/index/document.go +++ b/internal/index/document.go @@ -1,26 +1,23 @@ package index -import "github.com/svera/coreander/v4/internal/metadata" +import ( + "github.com/svera/coreander/v4/internal/metadata" +) type Document struct { metadata.Metadata - ID string - Slug string - Highlighted bool -} - -// DocumentWrite is an extension to Document that is used only when writing to the index, -// as some of its fields are only used to perform searches and not returned -type DocumentWrite struct { - Document - AuthorsEq []string - SeriesEq string - SubjectsEq []string + ID string + Slug string + AuthorsSlugs []string + SeriesSlug string + SubjectsSlugs []string + Highlighted bool + Type string } // BleveType is part of the bleve.Classifier interface and its purpose is to tell the indexer // the type of the document, which will be used to decide which analyzer will parse it. -func (d DocumentWrite) BleveType() string { +func (d Document) BleveType() string { if d.Language == "" { return "" } diff --git a/internal/metadata/epub.go b/internal/metadata/epub.go index 533523e..4230e77 100644 --- a/internal/metadata/epub.go +++ b/internal/metadata/epub.go @@ -83,6 +83,10 @@ func (e EpubReader) Metadata(file string) (Metadata, error) { for _, date := range meta.Date { if date.Event == "publication" || date.Event == "" { t, err := time.Parse("2006-01-02", date.Stamp) + if err != nil { + t, err = time.Parse("2006", date.Stamp) + } + if err == nil { year = strings.TrimLeft(t.Format("2006"), "0") break @@ -101,7 +105,7 @@ func (e EpubReader) Metadata(file string) (Metadata, error) { Year: year, Series: meta.Series, SeriesIndex: seriesIndex, - Type: "EPUB", + Format: "EPUB", Subjects: subjects, } w, err := words(file) diff --git a/internal/metadata/metadata.go b/internal/metadata/metadata.go index aac2519..2974eea 100644 --- a/internal/metadata/metadata.go +++ b/internal/metadata/metadata.go @@ -16,7 +16,7 @@ type Metadata struct { Series string SeriesIndex float64 Pages int - Type string + Format string Subjects []string } diff --git a/internal/metadata/pdf.go b/internal/metadata/pdf.go index d302cfb..f9b00bd 100644 --- a/internal/metadata/pdf.go +++ b/internal/metadata/pdf.go @@ -65,7 +65,7 @@ func (p PdfReader) Metadata(file string) (Metadata, error) { Language: lang, Year: year, Pages: pdf.GetPagesCount(), - Type: "PDF", + Format: "PDF", Subjects: []string{}, } diff --git a/internal/webserver/controller.go b/internal/webserver/controller.go index e6b64dd..090dcb1 100644 --- a/internal/webserver/controller.go +++ b/internal/webserver/controller.go @@ -5,6 +5,7 @@ import ( "github.com/svera/coreander/v4/internal/index" "github.com/svera/coreander/v4/internal/metadata" "github.com/svera/coreander/v4/internal/webserver/controller/auth" + "github.com/svera/coreander/v4/internal/webserver/controller/author" "github.com/svera/coreander/v4/internal/webserver/controller/document" "github.com/svera/coreander/v4/internal/webserver/controller/highlight" "github.com/svera/coreander/v4/internal/webserver/controller/home" @@ -19,6 +20,7 @@ type Controllers struct { Highlights *highlight.Controller Documents *document.Controller Home *home.Controller + Authors *author.Controller } func SetupControllers(cfg Config, db *gorm.DB, metadataReaders map[string]metadata.Reader, idx *index.BleveIndexer, sender Sender, appFs afero.Fs) Controllers { @@ -50,6 +52,10 @@ func SetupControllers(cfg Config, db *gorm.DB, metadataReaders map[string]metada UploadDocumentMaxSize: cfg.UploadDocumentMaxSize, } + authorsCfg := author.Config{ + WordsPerMinute: cfg.WordsPerMinute, + } + homeCfg := home.Config{ LibraryPath: cfg.LibraryPath, CoverMaxWidth: cfg.CoverMaxWidth, @@ -61,5 +67,6 @@ func SetupControllers(cfg Config, db *gorm.DB, metadataReaders map[string]metada Highlights: highlight.NewController(highlightsRepository, usersRepository, sender, cfg.WordsPerMinute, idx), Documents: document.NewController(highlightsRepository, sender, idx, metadataReaders, appFs, documentsCfg), Home: home.NewController(highlightsRepository, sender, idx, homeCfg), + Authors: author.NewController(highlightsRepository, sender, idx, authorsCfg), } } diff --git a/internal/webserver/controller/author/controller.go b/internal/webserver/controller/author/controller.go new file mode 100644 index 0000000..8f2f16a --- /dev/null +++ b/internal/webserver/controller/author/controller.go @@ -0,0 +1,40 @@ +package author + +import ( + "github.com/svera/coreander/v4/internal/index" + "github.com/svera/coreander/v4/internal/result" +) + +type Sender interface { + From() string +} + +// IdxReader defines a set of author reading operations over an index +type IdxReader interface { + SearchByAuthor(authorSlug string, page, resultsPerPage int) (result.Paginated[[]index.Document], error) + Author(slug string) (index.Author, error) +} + +type highlightsRepository interface { + HighlightedPaginatedResult(userID int, results result.Paginated[[]index.Document]) result.Paginated[[]index.Document] +} + +type Config struct { + WordsPerMinute float64 +} + +type Controller struct { + hlRepository highlightsRepository + idx IdxReader + sender Sender + config Config +} + +func NewController(hlRepository highlightsRepository, sender Sender, idx IdxReader, cfg Config) *Controller { + return &Controller{ + hlRepository: hlRepository, + idx: idx, + sender: sender, + config: cfg, + } +} diff --git a/internal/webserver/controller/author/search.go b/internal/webserver/controller/author/search.go new file mode 100644 index 0000000..ca57de8 --- /dev/null +++ b/internal/webserver/controller/author/search.go @@ -0,0 +1,68 @@ +package author + +import ( + "fmt" + "log" + "strconv" + + "github.com/gofiber/fiber/v2" + "github.com/svera/coreander/v4/internal/index" + "github.com/svera/coreander/v4/internal/result" + "github.com/svera/coreander/v4/internal/webserver/infrastructure" + "github.com/svera/coreander/v4/internal/webserver/model" + "github.com/svera/coreander/v4/internal/webserver/view" +) + +func (a *Controller) Search(c *fiber.Ctx) error { + emailSendingConfigured := true + if _, ok := a.sender.(*infrastructure.NoEmail); ok { + emailSendingConfigured = false + } + + var session model.Session + if val, ok := c.Locals("Session").(model.Session); ok { + session = val + } + + if session.WordsPerMinute > 0 { + a.config.WordsPerMinute = session.WordsPerMinute + } + + var searchResults result.Paginated[[]index.Document] + authorSlug := c.Params("slug") + + if authorSlug == "" { + return fiber.ErrBadRequest + } + + page, err := strconv.Atoi(c.Query("page")) + if err != nil { + page = 1 + } + + author, _ := a.idx.Author(authorSlug) + if searchResults, err = a.idx.SearchByAuthor(authorSlug, page, model.ResultsPerPage); err != nil { + log.Println(err) + return fiber.ErrInternalServerError + } + + if session.ID > 0 { + searchResults = a.hlRepository.HighlightedPaginatedResult(int(session.ID), searchResults) + } + + err = c.Render("author/results", fiber.Map{ + "Author": author, + "Results": searchResults, + "Paginator": view.Pagination(model.MaxPagesNavigator, searchResults, map[string]string{}), + "Title": fmt.Sprintf("Coreander - %s", author.Name), + "EmailSendingConfigured": emailSendingConfigured, + "EmailFrom": a.sender.From(), + "WordsPerMinute": a.config.WordsPerMinute, + }, "layout") + + if err != nil { + log.Println(err) + return fiber.ErrInternalServerError + } + return nil +} diff --git a/internal/webserver/controller/document/controller.go b/internal/webserver/controller/document/controller.go index 82bcf61..0c7a721 100644 --- a/internal/webserver/controller/document/controller.go +++ b/internal/webserver/controller/document/controller.go @@ -17,7 +17,7 @@ type Sender interface { // IdxReaderWriter defines a set of reading and writing operations over an index type IdxReaderWriter interface { Search(keywords string, page, resultsPerPage int) (result.Paginated[[]index.Document], error) - Count() (uint64, error) + Count(t string) (uint64, error) Close() error Document(Slug string) (index.Document, error) SameSubjects(slug string, quantity int) ([]index.Document, error) diff --git a/internal/webserver/controller/document/detail.go b/internal/webserver/controller/document/detail.go index 2ac81ab..f93966e 100644 --- a/internal/webserver/controller/document/detail.go +++ b/internal/webserver/controller/document/detail.go @@ -42,9 +42,8 @@ func (d *Controller) Detail(c *fiber.Ctx) error { } title := fmt.Sprintf("%s | Coreander", document.Title) - authors := strings.Join(document.Authors, ", ") - if authors != "" { - title = fmt.Sprintf("%s - %s | Coreander", authors, document.Title) + if len(document.Authors) > 0 { + title = fmt.Sprintf("%s - %s | Coreander", strings.Join(document.Authors, ", "), document.Title) } sameSubjects, err := d.idx.SameSubjects(document.Slug, relatedDocuments) diff --git a/internal/webserver/controller/home/controller.go b/internal/webserver/controller/home/controller.go index 945caf1..179797d 100644 --- a/internal/webserver/controller/home/controller.go +++ b/internal/webserver/controller/home/controller.go @@ -13,7 +13,7 @@ type Sender interface { // IdxReaderWriter defines a set of reading and writing operations over an index type IdxReaderWriter interface { Documents(IDs []string) (map[string]index.Document, error) - Count() (uint64, error) + Count(t string) (uint64, error) } type highlightsRepository interface { diff --git a/internal/webserver/controller/home/index.go b/internal/webserver/controller/home/index.go index 2054e7e..a66a0d6 100644 --- a/internal/webserver/controller/home/index.go +++ b/internal/webserver/controller/home/index.go @@ -4,6 +4,7 @@ import ( "log" "github.com/gofiber/fiber/v2" + "github.com/svera/coreander/v4/internal/index" "github.com/svera/coreander/v4/internal/webserver/infrastructure" ) @@ -13,7 +14,7 @@ func (d *Controller) Index(c *fiber.Ctx) error { emailSendingConfigured = false } - count, err := d.idx.Count() + count, err := d.idx.Count(index.TypeDocument) if err != nil { log.Println(err) return fiber.ErrInternalServerError diff --git a/internal/webserver/controller/user/create.go b/internal/webserver/controller/user/create.go index b639567..bc57f51 100644 --- a/internal/webserver/controller/user/create.go +++ b/internal/webserver/controller/user/create.go @@ -33,7 +33,7 @@ func (u *Controller) Create(c *fiber.Ctx) error { } if errs = user.ConfirmPassword(c.FormValue("confirm-password"), u.config.MinPasswordLength, errs); len(errs) > 0 { - return c.Render("users/new", fiber.Map{ + return c.Render("user/new", fiber.Map{ "Title": "Add user", "UsernamePattern": model.UsernamePattern, "Errors": errs, diff --git a/internal/webserver/controller/user/edit.go b/internal/webserver/controller/user/edit.go index 6bbe855..65e8f65 100644 --- a/internal/webserver/controller/user/edit.go +++ b/internal/webserver/controller/user/edit.go @@ -27,7 +27,7 @@ func (u *Controller) Edit(c *fiber.Ctx) error { return fiber.ErrForbidden } - return c.Render("users/edit", fiber.Map{ + return c.Render("user/edit", fiber.Map{ "Title": "Edit user", "User": user, "MinPasswordLength": u.config.MinPasswordLength, diff --git a/internal/webserver/controller/user/list.go b/internal/webserver/controller/user/list.go index f4f6513..8d62d28 100644 --- a/internal/webserver/controller/user/list.go +++ b/internal/webserver/controller/user/list.go @@ -16,7 +16,7 @@ func (u *Controller) List(c *fiber.Ctx) error { } users, _ := u.repository.List(page, model.ResultsPerPage) - return c.Render("users/index", fiber.Map{ + return c.Render("user/index", fiber.Map{ "Title": "Users", "Users": users.Hits(), "Paginator": view.Pagination(model.MaxPagesNavigator, users, map[string]string{}), diff --git a/internal/webserver/controller/user/new.go b/internal/webserver/controller/user/new.go index ffcb8f3..25395e4 100644 --- a/internal/webserver/controller/user/new.go +++ b/internal/webserver/controller/user/new.go @@ -10,7 +10,7 @@ func (u *Controller) New(c *fiber.Ctx) error { user := model.User{ WordsPerMinute: u.config.WordsPerMinute, } - return c.Render("users/new", fiber.Map{ + return c.Render("user/new", fiber.Map{ "Title": "Add user", "MinPasswordLength": u.config.MinPasswordLength, "User": user, diff --git a/internal/webserver/controller/user/update.go b/internal/webserver/controller/user/update.go index ba72dfa..764625b 100644 --- a/internal/webserver/controller/user/update.go +++ b/internal/webserver/controller/user/update.go @@ -51,7 +51,7 @@ func (u *Controller) updateUserData(c *fiber.Ctx, user *model.User, session mode } if len(validationErrs) > 0 { - return c.Status(fiber.StatusBadRequest).Render("users/edit", fiber.Map{ + return c.Status(fiber.StatusBadRequest).Render("user/edit", fiber.Map{ "Title": "Edit user", "User": user, "MinPasswordLength": u.config.MinPasswordLength, @@ -82,7 +82,7 @@ func (u *Controller) updateUserData(c *fiber.Ctx, user *model.User, session mode c.Locals("Session", user) } - return c.Render("users/edit", fiber.Map{ + return c.Render("user/edit", fiber.Map{ "Title": "Edit user", "User": user, "MinPasswordLength": u.config.MinPasswordLength, @@ -164,7 +164,7 @@ func (u *Controller) updateUserPassword(c *fiber.Ctx, user model.User, session m } if errs = user.ConfirmPassword(c.FormValue("confirm-password"), u.config.MinPasswordLength, errs); len(errs) > 0 { - return c.Render("users/edit", fiber.Map{ + return c.Render("user/edit", fiber.Map{ "Title": "Edit user", "User": user, "MinPasswordLength": u.config.MinPasswordLength, @@ -179,7 +179,7 @@ func (u *Controller) updateUserPassword(c *fiber.Ctx, user model.User, session m return fiber.ErrInternalServerError } - return c.Render("users/edit", fiber.Map{ + return c.Render("user/edit", fiber.Map{ "Title": "Edit user", "User": user, "MinPasswordLength": u.config.MinPasswordLength, diff --git a/internal/webserver/embedded/translations/es.yml b/internal/webserver/embedded/translations/es.yml index 5fcc1b1..704a837 100644 --- a/internal/webserver/embedded/translations/es.yml +++ b/internal/webserver/embedded/translations/es.yml @@ -135,3 +135,4 @@ "Session expired, please log in again.": "Sesión expirada, por favor identifícate de nuevo." "Unexpected error, check your connection and try to refresh the page.": "Error inesperado, comprueba tu conexión y recarga la página." "Unexpected server error": "Error inesperado en el servidor" +"Titles by %s": "Títulos de %s" diff --git a/internal/webserver/embedded/translations/fr.yml b/internal/webserver/embedded/translations/fr.yml index da1d205..db448ce 100644 --- a/internal/webserver/embedded/translations/fr.yml +++ b/internal/webserver/embedded/translations/fr.yml @@ -135,3 +135,4 @@ "Session expired, please log in again.": "Session expirée, veuillez vous reconnecter." "Unexpected error, check your connection and try to refresh the page.": "Erreur inattendue, vérifiez votre connexion et essayez d'actualiser la page." "Unexpected server error": "Erreur de serveur inattendue" +"Titles by %s": "Titres par %s" diff --git a/internal/webserver/embedded/views/author/results.html b/internal/webserver/embedded/views/author/results.html new file mode 100644 index 0000000..659694a --- /dev/null +++ b/internal/webserver/embedded/views/author/results.html @@ -0,0 +1,17 @@ +

{{t .Lang "Titles by %s" .Author.Name }}

+

+ {{if eq .Results.TotalHits 0}} + {{t .Lang "No matches found" }} + {{else}} + {{t .Lang "%d matches found" .Results.TotalHits }} + {{end}} +

+ +
+ {{ template "partials/docs-list" . }} +
+ + + + + diff --git a/internal/webserver/embedded/views/document.html b/internal/webserver/embedded/views/document.html index 139eb9c..407a4c5 100644 --- a/internal/webserver/embedded/views/document.html +++ b/internal/webserver/embedded/views/document.html @@ -16,7 +16,7 @@   {{t .Lang "Download"}} - {{.Document.Type}} + {{.Document.Format}} @@ -25,7 +25,7 @@   {{t .Lang "Download"}} - KEPUB + KEPUB {{if not .EmailSendingConfigured}} @@ -95,7 +95,7 @@

{{t $lang "Unknown author"}}

{{range $i, $subject := .Document.Subjects}} {{$subjectTitle := t $lang "Search for more titles in %s" $subject}} - {{$subject}} {{end}}
@@ -115,7 +115,7 @@

{{t $lang "Unknown author"}}

{{t $lang "Other documents in collection \"%s\"" .Document.Series}}

@@ -138,8 +138,13 @@

- + {{if gt (len $document.Authors) 1}} + {{t $lang "See all" }} + {{else}} + + {{t $lang "See all" }} + {{end}}
@@ -158,7 +163,7 @@

{{t $lang "Other documents with similar subjects"}}

- + {{t $lang "See all" }}
@@ -176,3 +181,4 @@

{{t $lang "Other documents with similar subjects"}}

+ diff --git a/internal/webserver/embedded/views/partials/actions.html b/internal/webserver/embedded/views/partials/actions.html index 9328d0f..39816c3 100644 --- a/internal/webserver/embedded/views/partials/actions.html +++ b/internal/webserver/embedded/views/partials/actions.html @@ -5,7 +5,7 @@   {{t .Lang "Download"}} - {{.Document.Type}} + {{.Document.Format}}
  • @@ -14,7 +14,7 @@   {{t .Lang "Download"}} - KEPUB + KEPUB
  • diff --git a/internal/webserver/embedded/views/partials/cover.html b/internal/webserver/embedded/views/partials/cover.html index 0347892..e5bf765 100644 --- a/internal/webserver/embedded/views/partials/cover.html +++ b/internal/webserver/embedded/views/partials/cover.html @@ -5,9 +5,9 @@ {{t .Lang "\"%s\" cover" .Document.Title}}
    -
    {{.Document.Title}}
    +
    {{.Document.Title}}
    {{if .Document.Authors}} -

    +

    {{join .Document.Authors ", "}}

    {{else}} diff --git a/internal/webserver/embedded/views/partials/docs-list.html b/internal/webserver/embedded/views/partials/docs-list.html index 4e7b6fb..649a45c 100644 --- a/internal/webserver/embedded/views/partials/docs-list.html +++ b/internal/webserver/embedded/views/partials/docs-list.html @@ -16,7 +16,7 @@ {{ if ne $document.Series "" }} {{$seriesTitle := t $lang "Search for more titles belonging to %s" $document.Series}}

    {{$document.Series}}{{ if ne $document.SeriesIndex 0.0 }} {{$document.SeriesIndex}}{{end}}

    {{ end }}

    @@ -40,7 +40,7 @@

    {{range $i, $author := $document.Authors}} {{$authorTitle := t $lang "Search for more titles by %s" $author}} - {{$author}}{{if notLast $document.Authors $i}}, {{end}} + {{$author}}{{if notLast $document.Authors $i}}, {{end}} {{end}}
    {{else}} @@ -60,7 +60,7 @@
    {{t $lang "Unknown author"}}
    {{range $i, $subject := $document.Subjects}} {{$subjectTitle := t $lang "Search for more titles in %s" $subject}} - {{$subject}} + {{$subject}} {{end}}
    {{ end }} diff --git a/internal/webserver/embedded/views/users/edit.html b/internal/webserver/embedded/views/user/edit.html similarity index 100% rename from internal/webserver/embedded/views/users/edit.html rename to internal/webserver/embedded/views/user/edit.html diff --git a/internal/webserver/embedded/views/users/index.html b/internal/webserver/embedded/views/user/index.html similarity index 100% rename from internal/webserver/embedded/views/users/index.html rename to internal/webserver/embedded/views/user/index.html diff --git a/internal/webserver/embedded/views/users/new.html b/internal/webserver/embedded/views/user/new.html similarity index 100% rename from internal/webserver/embedded/views/users/new.html rename to internal/webserver/embedded/views/user/new.html diff --git a/internal/webserver/routes.go b/internal/webserver/routes.go index eb8f3c6..2f7dc40 100644 --- a/internal/webserver/routes.go +++ b/internal/webserver/routes.go @@ -77,5 +77,6 @@ func routes(app *fiber.App, controllers Controllers, jwtSecret []byte, sender Se docsGroup.Get("/:slug", controllers.Documents.Detail) docsGroup.Get("/", controllers.Documents.Search) + app.Get("/authors/:slug", controllers.Authors.Search) app.Get("/", controllers.Home.Index) } diff --git a/internal/webserver/search_test.go b/internal/webserver/search_test.go index 064edb1..2130f4c 100644 --- a/internal/webserver/search_test.go +++ b/internal/webserver/search_test.go @@ -24,6 +24,7 @@ func TestSearch(t *testing.T) { }{ {"Search for documents with no metadata", "/documents?search=empty", 2}, {"Search for documents with metadata", "/documents?search=john+doe", 4}, + {"Search for authors", "/authors/john-doe", 4}, } for _, tcase := range cases {