你来你也可以做一个网盘搜索引擎

你来你也可以做一个网盘搜索引擎

前言

两年前笔者做过一个当时非常有成就感的小型搜索引擎,但回过头一看,全忘完了🙃,为了复习一下以前的知识,前面写过一篇文章总结概括了一下搜索引擎的原理。但作为程序员,光是理论肯定是不够的,所以得实践一下。以前的文章搜索引擎说实话没啥用,所以这次为了让项目更加有意思一点,决定做的是一个网盘类的搜索引擎,也算是有点用处。

笔者将整个搜索引擎简化了很多,甚至比两年前做的那个小型搜索引擎还要简单,但也算是一个微型搜索引擎了,帮助笔者实践了部分知识。

项目演示

线上演示地址:pan.justin3go.com

至文章发布3个月内该地址应该都可以访问。服务器暂时买了3个月的放在那里,3个月之后看是否还有空闲的服务器,否则应该就会下线该服务。

技术概览

让我们还是先看看之前这篇文章的一个架构图:

然后具体来说笔者使用了如下技术栈:

  1. 爬虫框架:Scrapy
  2. 索引存储/搜索:ElasticSearch
  3. 后端服务:NestJS
  4. 前端服务:VueJS
  5. 部署:Docker

爬虫

本章不会介绍Scrapy的使用,只会大致讲讲笔者的思路,框架使用请见Scrapy官网

  1. 找一些种子网站,比如知乎、贴吧中网盘资源分享的圈子
  2. 爬取该网页,文章类搜索引擎就是识别标题、正文之类的,而网盘类搜索引擎更加简单,直接使用正则表达式识别域名就可以了,比如阿里云盘的正则就是aliyundrive.com/s/[A-Za-z0-9]{11}
  3. 将提取的内容做处理并索引到ElasticSearch之中
  4. 提取该网页的所有外链,并过滤,比如资源链接(图片、视频、音乐)之类,继续爬取这些外链,重复该步骤

这里笔者将爬行深度限制为了10,因为一般离导航网站越远,越不重要,就节省资源,不爬取了。

python代码就不贴了,笔者已经很久没写过python代码了,这次写python代码基本上都是说说思路,然后GPT帮助笔者写的,反正感觉挺烂的,但总算Debug能力还在,把这堆代码跑起来了。

最后部署就用了node的pm2守护这个爬虫进程,放在服务器上一直运行就行了。

索引

部署方面使用Docker进行部署

部署流程笔者基本上都是参考的这篇文章,整体下来也比较简单,就不再重新部署演示了。

需要注意的是,不要为了贪图方便,不设置IP白名单,笔者就是因为家里网络属于动态IP,为了本地开发方便,就没有单独设置IP白名单,结果爬取了3周的数据全部被删除了,而且还不长记性,被删除了2次:

MD,我的Elasticsearch的索引全被删除了,之前想着没人知道我的IP地... #掘金沸点# juejin.cn/pin/7262761...

太猖狂了,删我ES数据库 - #掘金沸点# juejin.cn/pin/7264921...

所以这里要么使用iptables工具进行限制,或者直接使用云厂商的安全组策略,仅允许应用服务器的IP访问。

应用服务

最后就是应用服务了,这些就轻车熟路了。

后端部分

为了简单,笔者这里后端就只写了两个接口,一个是搜索的接口,一个是搜索建议的接口,就是输入前半截,提示后半截这种。

如下是NestJS中service部分:

ts 复制代码
import { Injectable } from '@nestjs/common';
import { ElasticsearchService } from '@nestjs/elasticsearch';
import { SearchDto } from './dto/search.dto';
import { SuggestDto } from './dto/suggest.dto';

@Injectable()
export class SearchService {
  constructor(private readonly elasticsearchService: ElasticsearchService) {}

  async search(data: SearchDto) {
    const { pageNo, pageSize, query } = data;
    const esRes = await this.elasticsearchService.search({
      index: 'pan',
      body: {
        from: (pageNo - 1) * pageSize, // 从哪里开始
        size: pageSize, // 查询条数
        query: {
          match: {
            title: query, // 搜索查询到的内容
          },
        },
        highlight: {
          pre_tags: ["<span class='highlight'>"],
          post_tags: ['</span>'],
          fields: {
            title: {},
          },
          fragment_size: 40,
        },
      },
    });
    // 组装参数
    const finalRes = {
      took: esRes.body.took,
      total: esRes.body.hits.total.value,
      data: esRes.body.hits?.hits.map((item: any) => ({
        title: item._source.title,
        pan_url: item._source.pan_url,
        extract_code: item._source.extract_code,
        highlight: item.highlight,
      })),
    };
    return finalRes;
  }

  async suggest(data: SuggestDto) {
    const { input } = data;
    const esRes = await this.elasticsearchService.search({
      index: 'pan',
      suggest_field: 'suggest',
      suggest_mode: 'always',
      suggest_size: 20,
      suggest_text: input,
    });
    return esRes.body.suggest;
  }
}

非常少,后端服务基本上就只写上面这几行代码,啥用户系统、安全限制都没做。没必要,只是为了演示,先做主要功能,如果有需要再迭代就行了,多半也不会继续开发了。

前端部分

整个前端页面就只有一个页面,也非常简单,稍微麻烦一点的就是兼容了一下移动端显示,UI库使用的是腾讯的UI库-TDesign,感觉还挺好看的,其他就没啥特别的了,如下是那个主页面的具体代码:

template部分:

html 复制代码
<template>
  <header>
    <div class="title">阿里云盘搜索神器</div>
    <div class="slogan">
      互联网具有天然的资源共享性,本小站不是资源的生产者,不存储任何资源,仅仅只是互联网的搬运工
    </div>
  </header>
  <main>
    <div class="search-input">
      <t-auto-complete
        v-model="searchValue"
        :options="options"
        placeholder="请输入关键词搜索"
        size="large"
        highlight-keyword
        filterable
        class="t-demo-autocomplete__search"
        @change="loadSuggest"
        @enter="searchData"
        @select="searchData"
      >
        <template v-if="searchValue" #suffix>
          <CloseCircleFilledIcon class="t-input__suffix-clear" @click="searchValue = ''" />
        </template>
        <template #suffixIcon>
          <t-button shape="square" size="large" @click="searchData"><SearchIcon /></t-button>
        </template>
      </t-auto-complete>
    </div>
    <t-loading size="small" :loading="loading" show-overlay style="width: 100%; height: 100%">
      <div class="list-container">
        <t-list :split="true">
          <t-list-item v-for="(item, index) in searchResult" :key="index">
            <t-list-item-meta :description="`提取码: ${item.extractCode || '--'}`">
              <template #title>
                <div class="result-title" v-html="item.highlight.title[0]"></div>
              </template>
            </t-list-item-meta>
            <template #action>
              <t-link
                theme="primary"
                hover="#000000"
                style="margin-left: 16px"
                @click="copyCode(item.extractCode)"
              >
                复制提取码
              </t-link>
              <t-link
                theme="primary"
                hover="#000000"
                style="margin-left: 16px"
                @click="openAdDialog(item.pan_url)"
              >
                跳转
              </t-link>
            </template>
          </t-list-item>
        </t-list>
        <div v-if="!searchResult.length" class="empty-container">
          <div class="empty">
            <!-- <WalletIcon size="80px" /> -->
            <img src="@/assets/empty.png" alt="" style="width: 240px; opacity: 0.5" />
            <div class="text">请输入搜索或暂无数据</div>
          </div>
        </div>
      </div>
    </t-loading>
    <div v-if="page.total" class="pagination-container">
      <t-pagination
        v-model="page.NO"
        v-model:pageSize="page.size"
        :total="page.total"
        :showPreviousAndNextBtn="false"
        page-ellipsis-mode="both-ends"
        @page-size-change="onPageSizeChange"
        @current-change="onCurrentChange"
      />
    </div>
  </main>
  <footer></footer>
  <t-dialog
    v-model:visible="visible"
    header="请认真阅读以下声明"
    confirmBtn="同意声明并跳转"
    @confirm="jumpLink"
    @close="closeAdDialog"
  >
    <p class="content">
      1.
      本站链接为程序自动收集自互联网,不储存、复制、传播、控制编辑任何网盘文件,不提供下载服务,链接跳转至官方网盘,文件的有效性和安全性需要您自行判断。
    </p>
    <p class="content">
      2. 本站坚决杜绝一切违规不良信息,如您发现任何涉嫌违规的网盘信息,请立即向<a
        href="https://terms.alicdn.com/legal-agreement/terms/suit_bu1_dingtalk/suit_bu1_dingtalk202103181300_11832.html"
        >网盘官方网站</a
      >举报
    </p>
    <p class="content">
      3. 本站是笔者在线作品演示网站,所有服务仅供学习交流使用,搜索引擎技术细节可以访问笔者的<a
        href="https://justin3go.com"
        >个人博客</a
      >查找。
    </p>
  </t-dialog>
</template>

script部分

html 复制代码
<script setup lang="ts">
import { reactive, ref, type Ref } from 'vue'
import { SearchIcon, CloseCircleFilledIcon } from 'tdesign-icons-vue-next'
import { MessagePlugin } from 'tdesign-vue-next'
import searchApi from '@/service/apis/search'
import useClipboard from 'vue-clipboard3'

const searchValue = ref('')
const options = ref([])

interface ISearchResultItem {
  title: string
  pan_url: string
  extractCode: string
  highlight: {
    title: string[]
  }
}

const searchResult: Ref<ISearchResultItem[]> = ref([])

const page = reactive({
  total: 0,
  NO: 1,
  size: 10
})

const loading = ref(false)

async function loadData() {
  if (loading.value) return
  loading.value = true
  const data = await searchApi.search({
    pageNo: page.NO,
    pageSize: page.size,
    query: searchValue.value
  })
  searchResult.value = data?.data?.data || []
  page.total = data?.data?.total || 0
  loading.value = false
}

function resetPage() {
  page.total = 0
  page.NO = 1
}

async function searchData() {
  resetPage()
  loadData()
}

const timer = ref()
function loadSuggest() {
  clearTimeout(timer.value)
  timer.value = setTimeout(async () => {
    const data = await searchApi.suggest({
      input: searchValue.value
    })
    const resOptions = data?.data?.suggest[0]?.options
    // eslint-disable-next-line no-control-regex
    options.value = resOptions?.map((item: any) => item.text.replace(/\u001f/g, '')) || []
  }, 200)
}

function onPageSizeChange(size: number) {
  page.size = size
  loadData()
}

function onCurrentChange(index: number) {
  page.NO = index
  loadData()
  MessagePlugin.success(`转到第${index}页`)
}

async function copyCode(code: string) {
  if (!code) {
    MessagePlugin.info('无需提取码')
  }
  const { toClipboard } = useClipboard()
  await toClipboard(code)
  MessagePlugin.success('复制成功')
}

const visible = ref(false)
const curPanUrl = ref('')
function openAdDialog(url: string) {
  visible.value = true
  curPanUrl.value = url
}

function closeAdDialog() {
  visible.value = false
}

function jumpLink() {
  let url = curPanUrl.value
  if (!url.startsWith('https://')) url = 'https://' + url
  window.open(url, '_blank')
}
</script>

css部分

scss 复制代码
<style lang="scss" scoped>
header {
  margin-top: 20px;
  padding: 0 100px;
  .title {
    font-size: 32px;
    font-weight: 900;
    text-align: center;
  }
  .slogan {
    text-align: center;
    color: #888888;
    margin-top: 10px;
    max-width: 80vw;

    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .slogan::before {
    content: '"';
    font-size: 40px;
    vertical-align: text-bottom;
    line-height: 20px;
  }
  .slogan::after {
    content: '"';
    font-size: 40px;
    vertical-align: text-top;
    /* line-height: 20px; */
  }
  /* 移动端样式适配 */
  @media (max-width: 1024px) {
    padding: 0;
    .slogan {
      max-width: 100vw;
    }
  }
}
:deep(.t-demo-autocomplete__search .t-input) {
  padding-right: 0;
}
:deep(.t-demo-auto-complete__base .t-button svg) {
  font-size: 20px;
}

main {
  position: relative;
  top: 0;
  .search-input {
    margin: 0 20%;
    width: 60%;
  }
  .list-container {
    margin: 20px 10% 50px 10%;
    width: 80%;
    height: calc(100vh - 250px);
    overflow-y: scroll;
    .empty-container {
      color: #bfbfbf;
      width: 100%;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      .empty {
        .text {
          width: 240px;
          text-align: center;
        }
      }
    }
  }
  .list-container::-webkit-scrollbar {
    width: 8px;
  }
  .list-container::-webkit-scrollbar-track {
    /* background: rgb(239, 239, 239); */
    border-radius: 2px;
  }
  .list-container::-webkit-scrollbar-thumb {
    background: #bfbfbf;
    border-radius: 10px;
  }
  .list-container::-webkit-scrollbar-thumb:hover {
    background: #a7a7a7;
  }

  .pagination-container {
    position: absolute;
    bottom: -50px;
    margin: 0 5%;
    width: 90%;
    background-color: #ffffff;
  }
  /* 移动端样式适配 */
  @media (max-width: 1024px) {
    padding: 0 20px;
    .search-input {
      margin: auto;
      width: calc(100vw - 40px);
    }
    .list-container {
      margin: 20px auto 50px auto;
      width: calc(100vw - 40px);
    }
    .pagination-container {
      width: calc(100% - 40px);
      margin: auto;
    }
  }
}
:deep(.highlight) {
  /* color: rgb(172, 141, 0); */
  color: rgb(138, 138, 230);
  /* font-weight: bolder; */
}

:deep(.t-dialog) {
  max-width: 360px;
}
</style>

还有一些其他零碎的代码就不贴了,主要页面就是上述部分。

最后

整体开发下来一个感受就是:由于人为简化了很多,所以每一部分,比如前端、后端、爬虫、部署之类的单独看都非常简单,但整个流程走下来却是较为复杂的,比如遇到BUG排查就会较为耗时。

项目应该不会开源,也没开源的必要,毕竟代码没怎么整理,比如前端项目用脚手架创建的AboutView.vue笔者都没删除。

相关推荐
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX3 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法4 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端