Repository: AlistGo/alist (language: go)

Issue:
Consider providing both FULLTEXT search & non-FULLTEXT search
### Please make sure of the following things

- [X] I have read the [documentation](https://alist.nn.ci).
- [X] I'm sure there are no duplicate issues or discussions.
- [X] I'm sure this feature is not implemented.
- [X] I'm sure it's a reasonable and popular requirement.

### Description of the feature / 需求描述

Provide the following search index:
- Bleve
- FULLTEXT Database
- Database
- None

Let the user decide to use FULLTEXT or not for now. FULLTEXT is faster for large db but I do not know how to make it usable for all the users.

### Suggested solution / 实现思路

_No response_

### Additional context / 附件

_No response_


Discussion / hints:
#2844 is not fixed for now in v3.8.0.

Fix patch:
diff --git a/internal/bootstrap/data/setting.go b/internal/bootstrap/data/setting.go
index 9057b4f2632..f6134884e56 100644
--- a/internal/bootstrap/data/setting.go
+++ b/internal/bootstrap/data/setting.go
@@ -143,7 +143,7 @@ func InitialSettings() []model.SettingItem {
 
 		// single settings
 		{Key: conf.Token, Value: token, Type: conf.TypeString, Group: model.SINGLE, Flag: model.PRIVATE},
-		{Key: conf.SearchIndex, Value: "none", Type: conf.TypeSelect, Options: "database,bleve,none", Group: model.INDEX},
+		{Key: conf.SearchIndex, Value: "none", Type: conf.TypeSelect, Options: "database,database_non_full_text,bleve,none", Group: model.INDEX},
 		{Key: conf.AutoUpdateIndex, Value: "false", Type: conf.TypeBool, Group: model.INDEX},
 		{Key: conf.IgnorePaths, Value: "", Type: conf.TypeText, Group: model.INDEX, Flag: model.PRIVATE, Help: `one path per line`},
 		{Key: conf.IndexProgress, Value: "{}", Type: conf.TypeText, Group: model.SINGLE, Flag: model.PRIVATE},
diff --git a/internal/db/searchnode.go b/internal/db/searchnode.go
index c93185b5c69..909d6452c83 100644
--- a/internal/db/searchnode.go
+++ b/internal/db/searchnode.go
@@ -54,21 +54,23 @@ func GetSearchNodesByParent(parent string) ([]model.SearchNode, error) {
 	return nodes, nil
 }
 
-func SearchNode(req model.SearchReq) ([]model.SearchNode, int64, error) {
+func SearchNode(req model.SearchReq, useFullText bool) ([]model.SearchNode, int64, error) {
 	var searchDB *gorm.DB
-	switch conf.Conf.Database.Type {
-	case "sqlite3":
+	if !useFullText || conf.Conf.Database.Type == "sqlite3" {
 		keywordsClause := db.Where("1 = 1")
 		for _, keyword := range strings.Fields(req.Keywords) {
 			keywordsClause = keywordsClause.Where("name LIKE ?", fmt.Sprintf("%%%s%%", keyword))
 		}
 		searchDB = db.Model(&model.SearchNode{}).Where(whereInParent(req.Parent)).Where(keywordsClause)
-	case "mysql":
-		searchDB = db.Model(&model.SearchNode{}).Where(whereInParent(req.Parent)).
-			Where("MATCH (name) AGAINST (? IN BOOLEAN MODE)", "'*" + req.Keywords + "*'")
-	case "postgres":
-		searchDB = db.Model(&model.SearchNode{}).Where(whereInParent(req.Parent)).
-			Where("to_tsvector(name) @@ to_tsquery(?)", strings.Join(strings.Fields(req.Keywords), " & "))
+	} else {
+		switch conf.Conf.Database.Type {
+		case "mysql":
+			searchDB = db.Model(&model.SearchNode{}).Where(whereInParent(req.Parent)).
+				Where("MATCH (name) AGAINST (? IN BOOLEAN MODE)", "'*"+req.Keywords+"*'")
+		case "postgres":
+			searchDB = db.Model(&model.SearchNode{}).Where(whereInParent(req.Parent)).
+				Where("to_tsvector(name) @@ to_tsquery(?)", strings.Join(strings.Fields(req.Keywords), " & "))
+		}
 	}
 	var count int64
 	if err := searchDB.Count(&count).Error; err != nil {
diff --git a/internal/search/db/search.go b/internal/search/db/search.go
index f3de07503e2..b6613e3b06e 100644
--- a/internal/search/db/search.go
+++ b/internal/search/db/search.go
@@ -15,7 +15,7 @@ func (D DB) Config() searcher.Config {
 }
 
 func (D DB) Search(ctx context.Context, req model.SearchReq) ([]model.SearchNode, int64, error) {
-	return db.SearchNode(req)
+	return db.SearchNode(req, true)
 }
 
 func (D DB) Index(ctx context.Context, node model.SearchNode) error {
diff --git a/internal/search/db_non_full_text/init.go b/internal/search/db_non_full_text/init.go
new file mode 100644
index 00000000000..9ac56fa3441
--- /dev/null
+++ b/internal/search/db_non_full_text/init.go
@@ -0,0 +1,16 @@
+package db
+
+import (
+	"github.com/alist-org/alist/v3/internal/search/searcher"
+)
+
+var config = searcher.Config{
+	Name:       "database_non_full_text",
+	AutoUpdate: true,
+}
+
+func init() {
+	searcher.RegisterSearcher(config, func() (searcher.Searcher, error) {
+		return &DB{}, nil
+	})
+}
diff --git a/internal/search/db_non_full_text/search.go b/internal/search/db_non_full_text/search.go
new file mode 100644
index 00000000000..7c88c758704
--- /dev/null
+++ b/internal/search/db_non_full_text/search.go
@@ -0,0 +1,45 @@
+package db
+
+import (
+	"context"
+
+	"github.com/alist-org/alist/v3/internal/db"
+	"github.com/alist-org/alist/v3/internal/model"
+	"github.com/alist-org/alist/v3/internal/search/searcher"
+)
+
+type DB struct{}
+
+func (D DB) Config() searcher.Config {
+	return config
+}
+
+func (D DB) Search(ctx context.Context, req model.SearchReq) ([]model.SearchNode, int64, error) {
+	return db.SearchNode(req, false)
+}
+
+func (D DB) Index(ctx context.Context, node model.SearchNode) error {
+	return db.CreateSearchNode(&node)
+}
+
+func (D DB) BatchIndex(ctx context.Context, nodes []model.SearchNode) error {
+	return db.BatchCreateSearchNodes(&nodes)
+}
+
+func (D DB) Get(ctx context.Context, parent string) ([]model.SearchNode, error) {
+	return db.GetSearchNodesByParent(parent)
+}
+
+func (D DB) Del(ctx context.Context, prefix string) error {
+	return db.DeleteSearchNodesByParent(prefix)
+}
+
+func (D DB) Release(ctx context.Context) error {
+	return nil
+}
+
+func (D DB) Clear(ctx context.Context) error {
+	return db.ClearSearchNodes()
+}
+
+var _ searcher.Searcher = (*DB)(nil)


Test patch: