From fa1cbdbfe35d9e6685f3d395fa70214dce644f30 Mon Sep 17 00:00:00 2001 From: Jianyun Date: Sat, 27 Jul 2024 09:11:47 +0800 Subject: [PATCH] add page --- Dockerfile | 6 +- README.md | 2 + config.yaml | 2 + internal/calibre/api.go | 213 ++++++++++++++++++++++++++++---- internal/calibre/sqlite.go | 19 ++- internal/calibre/sqlite_test.go | 125 +++++++++++++++++++ internal/calibre/types.go | 14 ++- main.go | 37 +++++- pages/static/.keep | 0 pages/templates/books.html | 116 +++++++++++++++++ pages/templates/detail.html | 146 ++++++++++++++++++++++ pages/templates/index.html | 102 +++++++++++++++ pages/templates/search.html | 116 +++++++++++++++++ pages/templates/setting.html | 65 ++++++++++ 14 files changed, 923 insertions(+), 40 deletions(-) create mode 100644 internal/calibre/sqlite_test.go create mode 100644 pages/static/.keep create mode 100644 pages/templates/books.html create mode 100644 pages/templates/detail.html create mode 100644 pages/templates/index.html create mode 100644 pages/templates/search.html create mode 100644 pages/templates/setting.html diff --git a/Dockerfile b/Dockerfile index 0f14e00..4cc17a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,10 +13,10 @@ RUN go build -o /calibre-api ## Deploy FROM gcr.io/distroless/base-debian10 -WORKDIR / - -COPY --from=build /calibre-api /calibre-api +WORKDIR /app +COPY --from=build /calibre-api ./calibre-api COPY config.yaml ./ +COPY pages/ ./ EXPOSE 8080 USER nonroot:nonroot diff --git a/README.md b/README.md index 8c1103d..40206ef 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,8 @@ storage: ```text CALIBRE_ADDRESS CALIBRE_DEBUG +CALIBRE_STATIC_DIR +CALIBRE_TEMPLATE_DIR ## search CALIBRE_SEARCH_HOST diff --git a/config.yaml b/config.yaml index 24fad05..2b33cd5 100644 --- a/config.yaml +++ b/config.yaml @@ -1,5 +1,7 @@ address: :8080 debug: false +static_dir: "./pages/static" +template_dir: "./pages/templates" search: host: http://127.0.0.1:7700 apikey: diff --git a/internal/calibre/api.go b/internal/calibre/api.go index ab47730..dd2d2ae 100644 --- a/internal/calibre/api.go +++ b/internal/calibre/api.go @@ -5,12 +5,12 @@ import ( "encoding/json" "fmt" "github.com/gin-gonic/gin" + "github.com/jianyun8023/calibre-api/pkg/log" "github.com/kapmahc/epub" "github.com/meilisearch/meilisearch-go" "io" "io/fs" "io/ioutil" - "log" "net/http" "os" "path" @@ -27,14 +27,18 @@ type Api struct { } func (c Api) SetupRouter(r *gin.Engine) { - r.GET("/get/cover/:id", c.getCover) - r.GET("/get/book/:id", c.getBookFile) - r.GET("/read/:id/toc", c.getBookToc) - r.GET("/read/:id/file/*path", c.getBookContent) - r.GET("/book/:id", c.getBook) - r.GET("/search", c.search) - r.POST("/search", c.search) - r.POST("/index/update", c.updateIndex) + + base := r.Group("/api") + base.GET("/get/cover/:id", c.getCover) + base.GET("/get/book/:id", c.getBookFile) + base.GET("/read/:id/toc", c.getBookToc) + base.GET("/read/:id/file/*path", c.getBookContent) + base.GET("/book/:id", c.getBook) + base.GET("/search", c.search) + base.POST("/search", c.search) + // 最近更新Recently + base.GET("/recently", c.recently) + base.POST("/index/update", c.updateIndex) } func NewClient(config *Config) Api { @@ -62,25 +66,81 @@ func NewClient(config *Config) Api { default: log.Fatal(fmt.Errorf("不支持的存储类型 %q", config.Storage.Use)) } + //index := client.Index(config.Search.Index) + index, err := ensureIndexExists(client, config.Search.Index) + if err != nil { + log.Fatal(err) + } + _, err = ensureIndexExists(client, config.Search.Index+"-bak") + if err != nil { + log.Fatal(err) + } return Api{ config: config, client: client, - bookIndex: client.Index(config.Search.Index), + bookIndex: index, fileClient: fileClient, baseDir: config.Storage.TmpDir, } } +// ensureIndexExists checks if a Meilisearch index exists, and if not, creates it and updates its settings. +// +// Parameters: +// - client: A pointer to the Meilisearch client. +// - indexName: The name of the index to check or create. +// +// Returns: +// - A pointer to the Meilisearch index. +// - An error if the index creation or settings update fails. +func ensureIndexExists(client *meilisearch.Client, indexName string) (*meilisearch.Index, error) { + index := client.Index(indexName) + + // Fetch index information to check if it exists + log.Infof("Checking if index %q exists", indexName) + _, err := index.FetchInfo() + if err != nil { + log.Infof("Failed to fetch index info for %q: %v", indexName, err) + // Index does not exist, create it + log.Infof("Creating index %q", indexName) + _, err = client.CreateIndex(&meilisearch.IndexConfig{ + Uid: indexName, + PrimaryKey: "id", + }) + if err != nil { + return nil, fmt.Errorf("failed to create index: %w", err) + } + log.Infof("Index %q created", indexName) + // Update index settings + log.Infof("Updating index settings for %q", indexName) + _, err = index.UpdateSettings(&meilisearch.Settings{ + DisplayedAttributes: []string{"*"}, + FilterableAttributes: []string{"authors", "file_path", "id", "last_modified", "pubdate", "publisher", "isbn", "tags"}, + SearchableAttributes: []string{"title", "authors"}, + SortableAttributes: []string{"authors_sort", "id", "last_modified", "pubdate", "publisher"}, + }) + if err != nil { + return nil, fmt.Errorf("failed to update index settings: %w", err) + } + } + return index, nil +} + func (c Api) search(r *gin.Context) { var req = meilisearch.SearchRequest{} err2 := r.Bind(&req) if err2 != nil { - log.Println("====== Only Bind By Query String ======", err2) + log.Infof("====== Only Bind By Query String ======", err2) } if len(req.Sort) == 0 { req.Sort = []string{"id:desc"} } + log.Infof("search request: %v", req) q := r.Query("q") + if q == "" { + q = r.PostForm("q") + } + log.Infof("search query: %s", q) search, err := c.bookIndex.Search(q, &req) books := make([]Book, len(search.Hits)) @@ -100,8 +160,8 @@ func (c Api) search(r *gin.Context) { return } id := book.ID - book.Cover = "/get/cover/" + strconv.FormatInt(id, 10) + ".jpg" - book.FilePath = "/get/book/" + strconv.FormatInt(id, 10) + ".epub" + book.Cover = "/api/get/cover/" + strconv.FormatInt(id, 10) + ".jpg" + book.FilePath = "/api/get/book/" + strconv.FormatInt(id, 10) + ".epub" books[i] = book } @@ -129,8 +189,8 @@ func (c Api) getBook(r *gin.Context) { r.JSON(http.StatusNotFound, "book not found") return } - book.Cover = "/get/cover/" + id + ".jpg" - book.FilePath = "/get/book/" + id + ".epub" + book.Cover = "/api/get/cover/" + id + ".jpg" + book.FilePath = "/api/get/book/" + id + ".epub" r.JSON(http.StatusOK, book) } @@ -230,13 +290,19 @@ func (c Api) getFile(filepath string) (os.FileInfo, io.ReadCloser, error) { return info, reader, nil } +func (c Api) stat(filepath string) (os.FileInfo, error) { + targetPath := path.Join(c.config.Storage.Webdav.Path, filepath) + info, err := c.fileClient.Stat(targetPath) + return info, err +} + func (c Api) getBookFile(r *gin.Context) { filesuffix := path.Ext(r.Param("id")) id := strings.TrimSuffix(r.Param("id"), filesuffix) var book Book err := c.bookIndex.GetDocument(id, nil, &book) if err != nil { - log.Println(err) + log.Warn(err) r.JSON(http.StatusInternalServerError, gin.H{ "code": 500, "message": "book not found", @@ -255,16 +321,24 @@ func (c Api) getCover(r *gin.Context) { err := c.bookIndex.GetDocument(id, nil, &book) if err != nil { - log.Println(err) + log.Warn(err) r.JSON(http.StatusInternalServerError, gin.H{ "code": 500, "message": "book not found", }) - } else { - info, reader, _ := c.getFile(c.fixPath(book.Cover)) - defer reader.Close() - r.DataFromReader(http.StatusOK, info.Size(), "", reader, nil) + return } + + info, reader, err := c.getFile(c.fixPath(book.Cover)) + if err != nil { + r.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": err.Error(), + }) + return + } + defer reader.Close() + r.DataFromReader(http.StatusOK, info.Size(), "image/jpeg", reader, nil) } func (c Api) getFileOrCache(filepath string, id string) (string, error) { @@ -295,9 +369,21 @@ func (c Api) getFileOrCache(filepath string, id string) (string, error) { func (c Api) getDbFileOrCache() (string, error) { filename := path.Join(c.baseDir, "metadata.db") - _, err := os.Stat(filename) + info, err := os.Stat(filename) + remoteInfo, err := c.stat("metadata.db") + if err != nil { + return "", err + } + if Exists(filename) { - return filename, nil + log.Info("cached metadata.db") + log.Infof("local file size: %d, remote file size: %d", info.Size(), remoteInfo.Size()) + if info.Size() == remoteInfo.Size() { + return filename, nil + } else { + log.Info("remove cached metadata.db") + _ = os.Remove(filename) + } } _, closer, err := c.getFile("metadata.db") if err != nil { @@ -307,7 +393,7 @@ func (c Api) getDbFileOrCache() (string, error) { if err != nil { return "", err } - closer.Close() + defer closer.Close() f, err := os.Create(filename) defer f.Close() @@ -324,14 +410,89 @@ func (c Api) updateIndex(c2 *gin.Context) { newDb, _ := NewDb(dbPath) books, _ := newDb.queryBooks() println(len(books)) - _, err = c.bookIndex.UpdateDocumentsInBatches(books, 20) + + index := c.client.Index(c.config.Search.Index + "-bak") + _, err = index.DeleteAllDocuments() if err != nil { - log.Println(err) + log.Warn(err) c2.JSON(http.StatusInternalServerError, err) return } + + _, err = index.UpdateDocumentsInBatches(books, 1000) + if err != nil { + log.Warn(err) + c2.JSON(http.StatusInternalServerError, err) + return + } + + resp, err := c.client.SwapIndexes( + []meilisearch.SwapIndexesParams{ + { + Indexes: []string{c.config.Search.Index, c.config.Search.Index + "-bak"}, + }, + }, + ) c2.JSON(http.StatusOK, gin.H{ "code": 200, "message": "success", + "data": resp, + }) +} + +func (c Api) recently(r *gin.Context) { + limit, err := strconv.Atoi(r.DefaultQuery("limit", "10")) + if err != nil { + r.JSON(http.StatusBadRequest, gin.H{"error": "Invalid limit"}) + return + } + offset, err := strconv.Atoi(r.DefaultQuery("offset", "0")) + if err != nil { + r.JSON(http.StatusBadRequest, gin.H{"error": "Invalid offset"}) + return + } + + searchRequest := meilisearch.SearchRequest{ + Sort: []string{"id:desc"}, + Limit: int64(limit), + Offset: int64(offset), + } + + search, err := c.bookIndex.Search("", &searchRequest) + if err != nil { + r.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + books := make([]Book, len(search.Hits)) + for i := range search.Hits { + tmp := search.Hits[i].(map[string]interface{}) + jsonb, err := json.Marshal(tmp) + if err != nil { + r.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + book := Book{} + if err := json.Unmarshal(jsonb, &book); err != nil { + r.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + id := book.ID + book.Cover = "/api/get/cover/" + strconv.FormatInt(id, 10) + ".jpg" + book.FilePath = "/api/get/book/" + strconv.FormatInt(id, 10) + ".epub" + books[i] = book + } + + r.JSON(http.StatusOK, gin.H{ + "totalHits": search.TotalHits, + "totalPages": search.TotalPages, + "hitsPerPage": search.HitsPerPage, + "estimatedTotalHits": search.EstimatedTotalHits, + "offset": search.Offset, + "limit": search.Limit, + "processingTimeMs": search.ProcessingTimeMs, + "query": search.Query, + "hits": &books, }) } diff --git a/internal/calibre/sqlite.go b/internal/calibre/sqlite.go index a30bc34..6c98451 100644 --- a/internal/calibre/sqlite.go +++ b/internal/calibre/sqlite.go @@ -2,6 +2,7 @@ package calibre import ( "database/sql" + "strings" ) import _ "github.com/mattn/go-sqlite3" @@ -11,6 +12,7 @@ const ( books.timestamp AS last_modified, books.pubdate AS pubdate, books.title AS title, + comments.text AS comments, group_concat(DISTINCT authors.name) AS authors, group_concat(DISTINCT authors.sort) AS authors_sort, group_concat(DISTINCT publishers.name) AS publisher, @@ -24,6 +26,8 @@ LEFT JOIN books_authors_link ON books.id = books_authors_link.book LEFT JOIN authors ON books_authors_link.author = authors.id +LEFT JOIN + comments ON comments.book = books.id LEFT JOIN books_publishers_link ON books.id = books_publishers_link.book LEFT JOIN @@ -70,15 +74,22 @@ func (d Db) queryBooks() (books []Book, err error) { // rows to books for rows.Next() { var book BookRaw - if err := rows.Scan(&book.ID, &book.LastModified, &book.Pubdate, &book.Title, &book.Authors, &book.AuthorSort, &book.Publisher, &book.FilePath, &book.Cover, &book.Size, &book.Isbn); err != nil { + if err := rows.Scan(&book.ID, &book.LastModified, &book.Pubdate, &book.Title, &book.Comments, &book.Authors, &book.AuthorSort, &book.Publisher, &book.FilePath, &book.Cover, &book.Size, &book.Isbn); err != nil { return nil, err } // convert BookRaw to Book newBook := Book{ - ID: book.ID, - AuthorSort: book.AuthorSort, - Authors: book.Authors, + ID: book.ID, + AuthorSort: book.AuthorSort, + Authors: func(authors string) []string { + authorList := strings.Split(authors, ",") + for i, author := range authorList { + authorList[i] = strings.TrimSpace(author) + } + return authorList + }(book.Authors), + Comments: book.Comments.String, Cover: book.Cover, FilePath: book.FilePath, Isbn: book.Isbn.String, diff --git a/internal/calibre/sqlite_test.go b/internal/calibre/sqlite_test.go new file mode 100644 index 0000000..ada7562 --- /dev/null +++ b/internal/calibre/sqlite_test.go @@ -0,0 +1,125 @@ +package calibre + +import ( + "database/sql" + "reflect" + "testing" +) + +func TestDb_Close(t *testing.T) { + type fields struct { + dbPath string + db *sql.DB + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &Db{ + dbPath: tt.fields.dbPath, + db: tt.fields.db, + } + if err := d.Close(); (err != nil) != tt.wantErr { + t.Errorf("Close() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestDb_queryBooks(t *testing.T) { + type fields struct { + dbPath string + db *sql.DB + } + tests := []struct { + name string + fields fields + wantBooks []Book + wantErr bool + }{ + // TODO: + { + name: "test queryBooks", + fields: fields{ + dbPath: "/Users/zhaojianyun/Downloads/metadata.db", + db: nil, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d, _ := NewDb(tt.fields.dbPath) + gotBooks, err := d.queryBooks() + + // 打印gotBooks + for _, book := range gotBooks { + t.Log(book) + } + if (err != nil) != tt.wantErr { + t.Errorf("queryBooks() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotBooks, tt.wantBooks) { + t.Errorf("queryBooks() gotBooks = %v, want %v", gotBooks, tt.wantBooks) + } + }) + } +} + +func TestNewCalibreDb(t *testing.T) { + type args struct { + dbPath string + } + tests := []struct { + name string + args args + want *Db + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewDb(tt.args.dbPath) + if (err != nil) != tt.wantErr { + t.Errorf("NewCalibreDb() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewCalibreDb() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getDb(t *testing.T) { + type args struct { + sqlite_path string + } + tests := []struct { + name string + args args + want *sql.DB + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getDb(tt.args.sqlite_path) + if (err != nil) != tt.wantErr { + t.Errorf("getDb() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getDb() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/calibre/types.go b/internal/calibre/types.go index 7d00aef..d963a64 100644 --- a/internal/calibre/types.go +++ b/internal/calibre/types.go @@ -7,7 +7,8 @@ import ( type Book struct { AuthorSort string `json:"author_sort"` - Authors string `json:"authors"` + Authors []string `json:"authors"` + Comments string `json:"comments"` Cover string `json:"cover"` FilePath string `json:"file_path"` ID int64 `json:"id"` @@ -25,6 +26,7 @@ type Book struct { type BookRaw struct { AuthorSort string `json:"author_sort"` Authors string `json:"authors"` + Comments sql.NullString `json:"comments"` Cover string `json:"cover"` FilePath string `json:"file_path"` ID int64 `json:"id"` @@ -42,10 +44,12 @@ type BookRaw struct { } type Config struct { - Address string `mapstructure:"address"` - Debug bool `mapstructure:"debug"` - Search Search `mapstructure:"search"` - Storage Storage `mapstructure:"storage"` + Address string `mapstructure:"address"` + Debug bool `mapstructure:"debug"` + StaticDir string `mapstructure:"static_dir"` + TemplateDir string `mapstructure:"template_dir"` + Search Search `mapstructure:"search"` + Storage Storage `mapstructure:"storage"` } type Search struct { Host string `mapstructure:"host"` diff --git a/main.go b/main.go index a67a1bc..b95634c 100644 --- a/main.go +++ b/main.go @@ -3,11 +3,10 @@ package main import ( "encoding/json" "fmt" + "github.com/gin-gonic/gin" "github.com/jianyun8023/calibre-api/internal/calibre" "github.com/jianyun8023/calibre-api/internal/lanzou" "github.com/jianyun8023/calibre-api/pkg/log" - - "github.com/gin-gonic/gin" "github.com/spf13/viper" "strings" ) @@ -21,10 +20,42 @@ func main() { gin.SetMode(gin.ReleaseMode) } r := gin.Default() + // 配置静态文件目录 + r.Static("/static", conf.StaticDir) + + // 配置模板目录 + //r.LoadHTMLGlob(conf.TemplateDir + "/*") + r.GET("/", func(c *gin.Context) { + //c.HTML(http.StatusOK, "index.html", nil) + c.File(conf.TemplateDir + "/index.html") + }) + // Serve the settings page + r.GET("/setting", func(c *gin.Context) { + c.File(conf.TemplateDir + "/setting.html") + //c.HTML(http.StatusOK, "setting.html", nil) + }) + + r.GET("/books", func(c *gin.Context) { + c.File(conf.TemplateDir + "/books.html") + //c.HTML(http.StatusOK, "setting.html", nil) + }) + + r.GET("/search", func(c *gin.Context) { + c.File(conf.TemplateDir + "/search.html") + //c.HTML(http.StatusOK, "search.html", nil) + }) + r.GET("/detail/:id", func(c *gin.Context) { + c.File(conf.TemplateDir + "/detail.html") + }) calibre.NewClient(conf).SetupRouter(r) if l, err := lanzou.NewClient(); err == nil { l.SetupRouter(r) } + // print router + for _, route := range r.Routes() { + log.Infof("route: %s %s", route.Method, route.Path) + } + log.Infof("server listen on %s", conf.Address) r.Run(conf.Address) } @@ -35,6 +66,8 @@ func initConfig() *calibre.Config { viper.AddConfigPath("$HOME/.calibre-api") viper.AddConfigPath(".") viper.SetDefault("address", ":8080") + viper.SetDefault("static_dir", "./pages/static") + viper.SetDefault("template_dir", "./pages/templates") viper.SetEnvPrefix("CALIBRE") viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AutomaticEnv() diff --git a/pages/static/.keep b/pages/static/.keep new file mode 100644 index 0000000..e69de29 diff --git a/pages/templates/books.html b/pages/templates/books.html new file mode 100644 index 0000000..f09c66a --- /dev/null +++ b/pages/templates/books.html @@ -0,0 +1,116 @@ + + + + + + + Book Store + + + + + +
+ +
+
+
+ +
+
+
+

全部书籍

+
+
+
+ 书籍封面 +
+

{{ truncateText(book.title) }}

+

{{ + truncateText(book.authors.join(', ')) + }}

+
+
+
+
+ + +
+
+ +
+ + + + + \ No newline at end of file diff --git a/pages/templates/detail.html b/pages/templates/detail.html new file mode 100644 index 0000000..e3f13ca --- /dev/null +++ b/pages/templates/detail.html @@ -0,0 +1,146 @@ + + + + + + + Book Detail + + + + + +
+ +
+
+
+ +
+
+

{{ book.title }}

+

ID: {{ book.id }} + +

+
+
+ Book Cover +
+

Authors: + {{ author }} +

+

Publisher:{{ + book.publisher}} +

+

ISBN: {{ book.isbn }}

+

Published Date: + {{ new Date(book.pubdate).toLocaleDateString() }}

+

Tags: {{ book.tags.join(',') }}

+

File Size: {{ + formatFileSize(book.size) }}

+ Download + Book + Delete Book +
+
+ +
+

简介

+

{{ book.comments }}

+
+
+
+ +
+ + + + + + \ No newline at end of file diff --git a/pages/templates/index.html b/pages/templates/index.html new file mode 100644 index 0000000..ff6fa48 --- /dev/null +++ b/pages/templates/index.html @@ -0,0 +1,102 @@ + + + + + + + Book Store + + + + + +
+ +
+
+
+ +
+
+
+

最近更新

+ 更多 +
+ +
+
+ 书籍封面 +
+

{{ truncateText(book.title) }}

+

{{ + truncateText(book.authors.join(', ')) + }}

+
+
+
+
+
+

文学

+
+ +
+
+
+

社会文化

+
+ +
+
+
+

历史

+
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/pages/templates/search.html b/pages/templates/search.html new file mode 100644 index 0000000..ee721a3 --- /dev/null +++ b/pages/templates/search.html @@ -0,0 +1,116 @@ + + + + + + Book Search + + + + +
+ +
+
+
+ +
+
+

搜索结果:{{ keyword }}

+
+
+ 书籍封面 +
+

{{ book.title }}

+

{{ book.authors.join(', ') }}

+

{{ book.publisher }}

+

{{ new Date(book.pubdate).toLocaleDateString() }}

+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/pages/templates/setting.html b/pages/templates/setting.html new file mode 100644 index 0000000..b607f8a --- /dev/null +++ b/pages/templates/setting.html @@ -0,0 +1,65 @@ + + + + + + Settings Page + + + + +
+ +
+
+

Settings

+ + + + + + + + + + + + + + + + +
SettingValueAction
Update IndexClick to update the search index
+
+ + + + \ No newline at end of file