前言

就我个人而言,不管多么吊的全文搜索,总是很不靠谱,关键时候总是掉链子,即使是买了昂贵的所谓的企业级的Elasticsearch,其提供的全文搜索功能也是很不靠谱,毕竟在维护基于es的日志搜索平台时,每天都有太多的人找我投诉搜不到想要的日志,而一去机器上用grep搜索的时候就有,具体细节我就不赘述了,所以我现在我只相信grep暴力搜索,好了不废话了,下面进入正题。

声明:

下面的代码全部基于Alist的一个老版本做的改动,这个版本是Version: v3.11.0-0-gfe416ba-dirty,如果你需要基于最新版本的Alist修改的话,应该可以举一反三。

修改完了之后参考下面的官方文档进行编译:

https://alist.nn.ci/zh/guide/install/source.html

预览

文件变动一览:

后端

  • 删除文件夹:internal/search/db_non_full_text 因为internal/bootstrap/data/setting.go中的key为conf.SearchIndex的options值中的database_non_full_text修改为了grep,所以这个文件夹就用不到了

  • 新增文件夹:internal/search/grep 这是新的grep搜索类型,要新增一个文件夹来实现

新增文件:

  • internal/search/grep/search.go grep搜索的核心代码
  • internal/search/grep/init.go grep搜索的init代码

修改文件:

  • internal/bootstrap/data/setting.go 用来把db_non_full_text逻辑替换成grep搜索逻辑
  • internal/search/import.go 用来导入新的grep搜索逻辑

前端

新增:无

修改文件:

  • src/pages/home/folder/Search.tsx 用来支持在新的标签页打开搜索结果中的html
  • src/pages/home/previews/index.ts 用来让html文件默认用html预览工具来打开

开干

玩的就是真实。

后端

后端新增文件internal/search/grep/search.go文件内容如下:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package db

import (
        "bufio"
        "context"
        "github.com/alist-org/alist/v3/drivers/local"
        "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"
        "github.com/alist-org/alist/v3/pkg/utils"
        "os/exec"
        "path/filepath"
        "strings"
)

type Grep struct{}

func (D Grep) Config() searcher.Config {
        return config
}

func (D Grep) ListLocalStorage() []model.Storage {
        storages, _, err := db.GetStorages(0, 500)
        var localStorages []model.Storage
        if err != nil {
                return localStorages
        }

        for i := range storages {
                storage := storages[i]
                if storage.Driver == "Local" {
                        localStorages = append(localStorages, storage)
                }
        }
        return localStorages
}

func (D Grep) FindRealRoot(parentFolder string) (string, string) {
        if len(parentFolder) <= 0 {
                return "", ""
        }
        localStorages := D.ListLocalStorage()
        if len(localStorages) <= 0 {
                return "", ""
        }
        for i := range localStorages {
                localStorage := localStorages[i]
                // Unmarshal Addition
                addition := &local.Addition{}
                err := utils.Json.UnmarshalFromString(localStorage.Addition, addition)
                if err != nil {
                        continue
                }
                if strings.Contains(parentFolder, localStorage.MountPath) {
                        return localStorage.MountPath, addition.RootFolderPath
                }
        }
        return "", ""
}

func (D Grep) Search(ctx context.Context, req model.SearchReq) ([]model.SearchNode, int64, error) {
        mountPath, rootFolderPath := D.FindRealRoot(req.Parent)
        if len(mountPath) == 0 || len(rootFolderPath) == 0 {
                return []model.SearchNode{}, 0, nil
        }

        realRootFolder := strings.Replace(
                req.Parent,
                mountPath,
                rootFolderPath,
                1,
        )
    kw:=req.Keywords
    isSpace:=strings.Contains(kw, " ")
    if isSpace == true {
        strSlice:=strings.Split(kw," ")
        formerStr:=strSlice[0]
        latterStr:=strSlice[1]
        kw=formerStr+".*"+latterStr+"|"+latterStr+".*"+formerStr
}
        cmd := exec.Command("grep", "-R", "-l", "-i", "-E", kw, realRootFolder)

        stderr, _ := cmd.StdoutPipe()
        cmd.Start()

        scanner := bufio.NewScanner(stderr)
        scanner.Split(bufio.ScanLines)
        var fileList []model.SearchNode
        var limit int = 0
        for scanner.Scan() {
                m := scanner.Text()
                fileName := strings.Split(m, ":")[0]
                cdir, cfile := filepath.Split(fileName)
                cfile = strings.TrimSuffix(cfile, "/")
                cdir = strings.Replace(cdir, rootFolderPath, mountPath, 1)

                if itemExists(fileList, cdir, cfile) {
                        continue
                }

                fileList = append(fileList, model.SearchNode{
                        Parent: cdir,
                        Name:   cfile,
                        IsDir:  false,
                        Size:   0,
                })
                limit++
                if limit >= 100 {
                        break
                }
        }
        cmd.Wait()
        return fileList, 0, nil
}

func itemExists(fileList []model.SearchNode, cdir string, cfile string) bool {
        for i := range fileList {
                file := fileList[i]
                if file.Parent == cdir && file.Name == cfile {
                        return true
                }
        }
        return false
}

func (D Grep) Index(ctx context.Context, node model.SearchNode) error {
        return nil
}

func (D Grep) BatchIndex(ctx context.Context, nodes []model.SearchNode) error {
        return nil
}

func (D Grep) Get(ctx context.Context, parent string) ([]model.SearchNode, error) {
        return []model.SearchNode{}, nil
}

func (D Grep) Del(ctx context.Context, prefix string) error {
        return nil
}

func (D Grep) Release(ctx context.Context) error {
        return nil
}

func (D Grep) Clear(ctx context.Context) error {
        return nil
}

var _ searcher.Searcher = (*Grep)(nil)

后端新增文件internal/search/grep/init.go文件内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package db

import (
        "github.com/alist-org/alist/v3/internal/search/searcher"
)

var config = searcher.Config{
        Name:       "grep",
        AutoUpdate: true,
}

func init() {
        searcher.RegisterSearcher(config, func() (searcher.Searcher, error) {
                return &Grep{}, nil
        })
}

后端修改文件internal/bootstrap/data/setting.go具体为:

把key为conf.SearchIndex的options值中的database_non_full_text修改为grep

后端修改文件internal/search/import.go具体为:

把db_non_full_text改为grep

前端

前端修改文件src/pages/home/folder/Search.tsx具体为:

在SearchResult这个函数下面的return属性中新增一个 target="_blank"属性,用来支持在新的标签页打开搜索结果中的html,示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const SearchResult = (props: SearchNode) => {
  return (
    <HStack
      w="$full"
      borderBottom={`1px solid ${hoverColor()}`}
      _hover={{
        bgColor: hoverColor(),
      }}
      rounded="$md"
      cursor="pointer"
      px="$2"
      as={LinkWithBase}
      href={props.path}
      target="_blank"
      encode
    >

前端修改文件src/pages/home/previews/index.ts具体为:

在函数getPreviews中将常量res改为变量:

从原来的 const res: PreviewComponent[] = []变为 var res: PreviewComponent[] = []

然后在// iframe previews注释上方新增如下代码,用来让html文件默认用html预览工具来打开:

1
2
3
4
 var fileExt = ext(file.name).toLowerCase()
  if (fileExt == "html") {
    res = res.filter(item => item.name === "HTML render")
  }

完整示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export const getPreviews = (
  file: Obj & { provider: string }
): PreviewComponent[] => {
  var res: PreviewComponent[] = []
  // internal previews
  previews.forEach((preview) => {
    if (preview.provider && !preview.provider.test(file.provider)) {
      return
    }
    if (
      preview.type === file.type ||
      preview.exts === "*" ||
      preview.exts?.includes(ext(file.name).toLowerCase())
    ) {
      res.push({ name: preview.name, component: preview.component })
    }
  })
  var fileExt = ext(file.name).toLowerCase()
  if (fileExt == "html") {
    res = res.filter(item => item.name === "HTML render")
  }
  // iframe previews
  const iframePreviews = getIframePreviews(file.name)
  iframePreviews.forEach((preview) => {
    res.push({
      name: preview.key,
      component: generateIframePreview(preview.value),
    })
  })
  // download page
  res.push({
    name: "Download",
    component: lazy(() => import("./download")),
  })
  return res
}

下面是如何实现支持中文:

https://crowdin.com/backend/download/project/alist/zh-CN.zip这里下载中文语言包,解压后放到前端文件夹src/lang下面,然后返回前端项目根目录执行 node ./scripts/i18n.mjs,

此时src/lang/zh-CN下面会生成一个叫entry.ts的文件然后重新编译前端项目即可。

注意事项:

要把虚拟机内存调成4个G,然后执行如下命令:set NODE_OPTIONS=–max_old_space_size=4096