在 NAS 上部署一个属于个人的磁力搜索引擎

110 天前
 journey0ad

前段时间想给 NAS 找点事情做,发现了 Bitmagnet 这个项目,用了几天发现爬虫的部分还不错,但是自带的 webui 有点简陋,有权限敞开、种子数量上来后搜索慢等问题,用起来总是不那么爽

于是花了几天时间用 Nextjs + NextUI 写了个前端界面,顺带也是为了学一下 Next 的开发,界面功能参考了常见的一些磁力搜索引擎,顺带做了夜间模式、多语言、内容预览和搜索分词等功能

详细的部署过程可以看仓库,提供有 Docker Compose 配置可以快速部署,也可以点 Demo 链接体验,Demo 为了避免版权等问题,内容是固定的

仓库地址: https://github.com/journey-ad/Bitmagnet-Next-Web
Demo: https://bitmagnet-next-web.vercel.app/

预览:

下面是一些开发过程的记录:

原版的 Bitmagnet 自带一个 GraphQL 的 api 可以进行搜索,但用下来和 webui 遇到的问题是一样的,数量上去之后普遍搜索时间在几十秒,最重要的是返回的结果数量不准,没办法做分页

关于搜索和索引问题我问过 bitmagnet 的开发者,他回复是已经结合种子标题和文件内容,在 torrent_contents.tsv 里创建了向量索引。研究后发现是先转罗马音然后存的向量,这样好处是可以用 pg 原生支持的 tsquery 来查询,而且转成罗马音后对应中文的是拼音,可以做到错字也能搜到,坏处是只要同音字就能搜到,就算同音字很离谱也是一样

另外就是 bitmagnet 用了 go-unidecode 这个库做罗马音化,但这个库在 node 上没有一比一对应的,转罗马音的过程和 bitmagnet 做不到完全一致,影响搜索效果

综合以上问题,决定自己写后端代码直连 DB 来查询,但之前没怎么搞过后端,更没搞过搜索这种东西,总之边写边测边改,搞出来了 gin 索引+传统 like 模糊匹配+分词,并根据每个关键词的词性确定为必须或非必须,生成对应的 SQL 查询这种野路子方案。至于为什么不上 ES ,还有个考虑是想侵入性的修改尽量少点,后面 bitmagnet 库表结构有升级时好适配,所以没选 ES 这种重的方案

结果比较多的情况下基本在几百毫秒到几秒内就能返回,部分收录少的词可能要跑全表,要等十几秒,这个性能还算能接受,暂时是我能想到的最好的方案了,等有空了研究一下 discuz 之类的论坛怎么做的搜索

3052 次点击
所在节点    分享创造
10 条回复
chunkingName
110 天前
是的,我目前 nas 上爬了三百多万种子,自带的搜索很慢,前几天连续搜索出错了,现在一直报错不进行搜索了,说数据库处于查询状态。试试你这个
pxiphx891
110 天前
点赞,这个好
chunkingName
110 天前
搜索报这个错,用他原来的 web 搜索正常

发生意外错误
Message: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.

Digest: 1841269720
journey0ad
110 天前
@chunkingName #3
看下容器内的日志?
chunkingName
110 天前
@journey0ad date stream content
2024/07/16 14:44:54 stderr }
2024/07/16 14:44:54 stderr digest: '1841269720'
2024/07/16 14:44:54 stderr at async y (/app/.next/server/app/search/page.js:1:11391) {
2024/07/16 14:44:54 stderr at async b (/app/.next/server/app/search/page.js:1:10813)
2024/07/16 14:44:54 stderr at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
2024/07/16 14:44:54 stderr at a (/app/.next/server/chunks/200.js:1:25217)
2024/07/16 14:44:54 stderr Error: Network response was not ok: Internal Server Error
2024/07/16 14:44:54 stderr at async y (/app/.next/server/app/search/page.js:1:11391)
2024/07/16 14:44:54 stderr at async b (/app/.next/server/app/search/page.js:1:10813)
2024/07/16 14:44:54 stderr at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
2024/07/16 14:44:54 stderr at a (/app/.next/server/chunks/200.js:1:25217)
2024/07/16 14:44:54 stderr Error: Network response was not ok: Internal Server Error
2024/07/16 14:44:54 stderr Failed to fetch: Network response was not ok: Internal Server Error
2024/07/16 14:44:54 stdout keywords: [ { _: '$1', keyword: '雷米', required: true } ]
2024/07/16 14:44:54 stderr }
2024/07/16 14:44:54 stderr extraInfo: undefined
2024/07/16 14:44:54 stderr networkError: null,
2024/07/16 14:44:54 stderr clientErrors: [],
2024/07/16 14:44:54 stderr protocolErrors: [],
2024/07/16 14:44:54 stderr ],
2024/07/16 14:44:54 stderr }
2024/07/16 14:44:54 stderr extensions: [Object]
2024/07/16 14:44:54 stderr path: [Array],
2024/07/16 14:44:54 stderr locations: [Array],
2024/07/16 14:44:54 stderr message: 'Failed to execute search query',
2024/07/16 14:44:54 stderr {
2024/07/16 14:44:54 stderr graphQLErrors: [
2024/07/16 14:44:54 stderr at t.next (/app/.next/server/chunks/38.js:1:134768) {
2024/07/16 14:44:54 stderr at b (/app/.next/server/chunks/38.js:1:134267)
2024/07/16 14:44:54 stderr at g (/app/.next/server/chunks/38.js:1:133726)
2024/07/16 14:44:54 stderr at Object.next (/app/.next/server/chunks/38.js:1:39726)
2024/07/16 14:44:54 stderr at Object.then (/app/.next/server/chunks/38.js:1:39598)
2024/07/16 14:44:54 stderr at new Promise (<anonymous>)
2024/07/16 14:44:54 stderr at /app/.next/server/chunks/38.js:1:39631
2024/07/16 14:44:54 stderr at o (/app/.next/server/chunks/38.js:1:39716)
2024/07/16 14:44:54 stderr at /app/.next/server/chunks/38.js:1:76524
2024/07/16 14:44:54 stderr at new t (/app/.next/server/chunks/38.js:1:88954)
2024/07/16 14:44:54 stderr t [ApolloError]: Failed to execute search query
2024/07/16 14:44:54 stderr }
2024/07/16 14:44:54 stderr port: 5433
2024/07/16 14:44:54 stderr address: '192.168.11.2',
2024/07/16 14:44:54 stderr syscall: 'connect',
2024/07/16 14:44:54 stderr code: 'ECONNREFUSED',
2024/07/16 14:44:54 stderr errno: -111,
2024/07/16 14:44:54 stderr at async Object.g [as search] (/app/.next/server/app/api/graphql/route.js:131:107) {
2024/07/16 14:44:54 stderr at async Promise.all (index 1)
2024/07/16 14:44:54 stderr at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
2024/07/16 14:44:54 stderr at /app/node_modules/pg-pool/index.js:45:11
2024/07/16 14:44:54 stderr Error in search resolver: Error: connect ECONNREFUSED 192.168.11.2:5433
2024/07/16 14:44:54 stdout [ '%雷米%', 10, 0 ]
2024/07/16 14:44:54 stdout filtered; -- 从过滤后的数据中查询
2024/07/16 14:44:54 stdout FROM
2024/07/16 14:44:54 stdout END AS files -- 结果别名设为 'files'
2024/07/16 14:44:54 stdout ELSE NULL -- 如果 files_count 为空, 则设置为 NULL
2024/07/16 14:44:54 stdout )
2024/07/16 14:44:54 stdout WHERE torrent_files.info_hash = filtered.info_hash -- 根据 info_hash 匹配文件
2024/07/16 14:44:54 stdout FROM torrent_files
2024/07/16 14:44:54 stdout ))
2024/07/16 14:44:54 stdout 'extension', torrent_files.extension -- 文件扩展名
2024/07/16 14:44:54 stdout 'size', torrent_files.size, -- 文件大小
2024/07/16 14:44:54 stdout 'path', torrent_files.path, -- 文件在种子中的路径
2024/07/16 14:44:54 stdout 'index', torrent_files.index, -- 文件在种子中的索引
2024/07/16 14:44:54 stdout SELECT json_agg(json_build_object(
2024/07/16 14:44:54 stdout -- 如果有数量, 根据 info_hash 查询文件信息到 'files' 列, 聚合成 JSON
2024/07/16 14:44:54 stdout WHEN filtered.files_count IS NOT NULL THEN (
2024/07/16 14:44:54 stdout CASE
2024/07/16 14:44:54 stdout -- 检查 files_count, 是否有文件数量
2024/07/16 14:44:54 stdout filtered.files_count, -- 种子文件数
2024/07/16 14:44:54 stdout filtered.updated_at, -- 更新时间戳
2024/07/16 14:44:54 stdout filtered.created_at, -- 创建时间戳
2024/07/16 14:44:54 stdout filtered.size, -- 种子大小
2024/07/16 14:44:54 stdout filtered.name, -- 种子名称
2024/07/16 14:44:54 stdout filtered.info_hash, -- 种子哈希
2024/07/16 14:44:54 stdout SELECT
2024/07/16 14:44:54 stdout -- 从过滤后的数据中查询文件信息
2024/07/16 14:44:54 stdout )
chunkingName
110 天前
@journey0ad 我将默认数据库 5432 改为了 5433 其余的没动 原版网页搜索正常,你的这个报日志这些错误
journey0ad
110 天前
@chunkingName #6
看上去是 webui 到数据库容器的连接不通

是用 docker compose 部署的吗? docker 内有网络隔离,db 连接串的 host 部分需要和 postgres 所在的容器名一致才能连接上,参考 https://github.com/journey-ad/Bitmagnet-Next-Web/blob/1981a093cea8291e476fadab82bdf4b07bc207a4/docker-compose.yml
43 行的容器名和暴露的端口号,和 11 行 db 连接串的 postgres:5432 是对应的,29 行的 POSTGRES_HOST=postgres 也是一样的道理

或者不管用什么方法,保证 webui 的容器到 postgres 容器是可联通的就行
lisawang
106 天前
我在宝塔里用直接复制的 docker-compose.yml 用 Docker Compose 部署,ip:3000 搜索就是错误,但是:3333 是没问题,进去 ping 了一下 PostgreSQL ,ping 的通,奇怪,怎么解决呢
发生意外错误
Message: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.

Digest: 2644143229
lisawang
106 天前
TypeError: fetch failed
at node:internal/deps/undici/undici:12618:11
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async a (/app/.next/server/chunks/200.js:1:25185)
at async b (/app/.next/server/app/search/page.js:1:10813)
at async y (/app/.next/server/app/search/page.js:1:11391) {
cause: Error: connect ECONNREFUSED ::1:3000
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1555:16)
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:128:17) {
errno: -111,
code: 'ECONNREFUSED',
syscall: 'connect',
address: '::1',
port: 3000
},
digest: '2644143229'
}
错误日志
journey0ad
104 天前

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1057681

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX