溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

用elasticsearch和nuxtjs搭建bt搜索引擎

發(fā)布時間:2020-08-05 01:37:21 來源:網(wǎng)絡(luò) 閱讀:9443 作者:withcancer 欄目:軟件技術(shù)

世界上已經(jīng)有了這么多種子搜索引擎,為什么你還要不厭其煩的做一個新的?

可以這么說,地球上大多數(shù)的種子搜索引擎的前后端技術(shù)都比較古老,雖然古老的技術(shù)既經(jīng)典又好用,但是作為一個喜歡嘗鮮的人,我仍然決定使用目前最為先進(jìn)的開發(fā)技術(shù)制作一個功能簡明的種子搜索引擎。

采用了什么技術(shù)?

前端:在vue,angular,react三大現(xiàn)×××發(fā)框架中選擇了vue,做出這個決定的原因也僅僅是一直以來對vue的謎之好感。有時候就是這樣,緣分到了不得不從,恰巧nuxtjs在9月更新了2.0,因此毫不猶豫選擇了vue。
后端:在koa,gin,springboot中權(quán)衡良久,由于很長時間沒有寫過java,最后選擇了springboot + jdk11,用寫javascript的感覺來寫java,還是很不錯的。從追求速度上來講,可能使用gin或Koa要更快,但是這一點(diǎn)提升對于我這種實(shí)驗(yàn)性網(wǎng)站來說,意義并不是很大。
全文檢索:嘗試了全文檢索里面的比較潮的couchbase、redissearch、elasticsearch,最后選定elasticsearch,另外兩個的速度雖然遠(yuǎn)高于elasticsearch,但畢竟是內(nèi)存式數(shù)據(jù)庫,簡單功能尚可,復(fù)雜度上去后則吃內(nèi)存太多。

制作過程呢?

下面我分享下大概過程,涉及到復(fù)雜原理,請自行谷歌,我不認(rèn)為我可以把復(fù)雜原理描述的很簡單。

關(guān)于命名:

從手中的十來個域名選擇了

btzhai.top

中國國內(nèi)同名的網(wǎng)站有幾個,但是這不是問題。

關(guān)于服務(wù)器

幾經(jīng)周折,購買了一臺美國服務(wù)器。配置是:E5-1620|24G|1TB|200M帶寬,真正的24小時人工服務(wù)??紤]到要用cloudfare,所以不需要硬防。一月1200RMB。

在此期間嘗試了很多家服務(wù)器,深感這免備案服務(wù)器這一行真的是泥沙俱下。

關(guān)于爬蟲:

大約8月初終于有空來著手bt搜索引擎這件事情。

首先擺在我面前的問題就是數(shù)據(jù)來源問題,要知道所謂的dht網(wǎng)絡(luò),說白了就是一個節(jié)點(diǎn)既是服務(wù)器又是客戶端,你在利用dht網(wǎng)絡(luò)下載時會廣播到網(wǎng)絡(luò)中,別的節(jié)點(diǎn)就會接收到你所下載文件的唯一標(biāo)識符infohash(有的地方稱之為神秘代碼)和metadata,這里面包括了這個文件的名稱、大小、創(chuàng)建時間、包含文件等信息,利用這個原理,dht爬蟲就可以收集dht網(wǎng)絡(luò)中的即時熱門下載。

如果僅僅依靠依靠dht爬蟲去爬的話,理論上初期速度大約為40w一天,30天可以收集上千萬,但是dht網(wǎng)絡(luò)里面的節(jié)點(diǎn)不可能總是下載新的文件,現(xiàn)實(shí)情況是:大多數(shù)情況下冷門的種子幾年無人問津,熱門種子天天數(shù)十萬人下載??梢酝葡?,隨著種子基數(shù)增加,重復(fù)的infohash會越來越多,慢慢地只會增加所謂的種子熱度而不會增加基數(shù),但是沒有1000w+的種子,從門面上來講不好看。

去哪里弄1000w種子成了當(dāng)時我主要研究的問題。首先我從github上選取了幾個我認(rèn)為比較好用的dht爬蟲進(jìn)行改造,讓之可以直接將數(shù)據(jù)入庫到elasticsearch中,并且在infohash重復(fù)的時候自動對熱度+1。

elasticsearch的mapping如下,考慮到中文分詞,選用了smartcn作為分詞器,當(dāng)然ik也是可以的。種子內(nèi)的文件列表files,本來設(shè)置為nested object,因?yàn)閚ested query性能不高已經(jīng)取消:

{
    "properties": {
        "name": {
            "type": "text",
            "analyzer": "smartcn",
            "search_analyzer": "smartcn"
        },
        "length": {
            "type": "long"
        },
        "popularity": {
            "type": "integer"
        },
        "create_time": {
            "type": "date",
            "format": "epoch_millis"
        },
        "files": {
            "properties": {
                "length": {
                    "type": "long"
                },
                "path": {
                    "type": "text",
                    "analyzer": "smartcn",
                    "search_analyzer": "smartcn"
                }
            }
        }
    }
}

服務(wù)器上開始24小時掛著dht爬蟲。期間我也嘗試過多種不同語言的開源爬蟲來比較性能,甚至還找人試圖購買bt種子。下面這些爬蟲我都實(shí)際使用過:

https://github.com/shiyanhui/dht
https://github.com/fanpei91/p2pspider
https://github.com/fanpei91/simDHT
https://github.com/keenwon/antcolony
https://github.com/wenguonideshou/zsky

然而這些dht爬蟲經(jīng)試驗(yàn),或多或少都有些問題,有的是只能采集infohash而不能采集metadata,有的采集速度不夠,有的則隨時間增加資源占用越來越大。

最終確定的是這個最優(yōu)解:

https://github.com/neoql/btlet

唯一不妥是運(yùn)行一段時間(大約10個小時)后就會崩潰退出,可能跟采集速度有關(guān)。而在我寫這篇文章的前幾天,作者稱已經(jīng)將此問題修復(fù),我還沒有來得及跟進(jìn)更新??梢哉f這是我實(shí)驗(yàn)過采集速度最快的dht爬蟲。有興趣的同學(xué)可以去嘗試、PR。

爬蟲正常化運(yùn)行以后,我終于發(fā)現(xiàn)了基數(shù)問題的解決之道,那就是skytorrent關(guān)閉后dump出來的數(shù)據(jù)庫和openbay,利用這大約4000w infohash數(shù)據(jù)和bthub,每天都一定可以保證有數(shù)萬新的metadata入庫。

關(guān)于bthub我要說的是,api請求頻率太高會被封ip,發(fā)郵件詢問的結(jié)果如下。經(jīng)過我的反復(fù)測試,api請求間隔設(shè)為1s也是沒問題的:
用elasticsearch和nuxtjs搭建bt搜索引擎

關(guān)于前端:

我比較習(xí)慣于先畫出簡單的前端再開始寫后端,前端確定清楚功能以后就可以很快寫出對應(yīng)的接口。bt搜索引擎目前具有以下這么幾個功能就足夠了:

  1. 可以搜索關(guān)鍵詞

  2. 首頁可以展現(xiàn)之前搜索過的排行前十的關(guān)鍵詞

  3. 可以隨機(jī)推薦一些文件

  4. 可以按照相關(guān)性、大小、創(chuàng)建時間、熱度排序

首頁啟動時,為了提高速度,從后臺讀cache,包括收錄了多少infohash、隨機(jī)推薦的文件名稱、搜索關(guān)鍵詞top10等等,這些cache使用@Scheduled每天自動更新一次。

點(diǎn)擊搜索后,跳轉(zhuǎn)到結(jié)果展現(xiàn)頁面,這里只展現(xiàn)elasticsearch處理過highlight之后的結(jié)果而不展現(xiàn)所有原始結(jié)果,每頁展示10個結(jié)果。

原始結(jié)果的展現(xiàn)放在最后一個詳細(xì)畫面上。

前端承載的另一個重要問題就是seo,這也是我使用nuxtjs的原因。前端功能完成以后,我為它添加了meta描述、google analytics、百度。

sitemap的添加倒是耗廢了一些時間,因?yàn)槭莿討B(tài)網(wǎng)頁的緣故,只能用nuxt-sitemap來動態(tài)生成。

另外用媒體查詢和vh、vw做了移動適配。不敢說100%,至少可以覆蓋90%的設(shè)備。

關(guān)于后端:

spring data在實(shí)現(xiàn)核心搜索api時遇到了問題,核心搜索如果寫成json,舉個例子的話,可能是下面的這個樣子:

{
    "from": 0,
    "size": 10,
    "sort": [{
        "_score": "desc"
    }, {
        "length": "desc"
    }, {
        "popularity": "desc"
    }, {
        "create_time": "desc"
    }],
    "query": {
        "multi_match": {
            "query": "這里是要搜索的關(guān)鍵詞",
            "fields": ["name", "files.path"]
        }
    },
    "highlight": {
        "pre_tags": ["<strong>"],
        "post_tags": ["</strong>"],
        "fields": {
            "name": {
                "number_of_fragments": 1,
                "no_match_size": 150
            },
            "files.path": {
                "number_of_fragments": 3,
                "no_match_size": 150
            }
        }
    }
}

highlight返回的結(jié)果將沒有辦法自動和entity匹配,因?yàn)檫@一部分?jǐn)?shù)據(jù)不在source中,spring data無法通過getSourceAsMap來獲取。這里需要用到NativeSearchQueryBuilder去手動配置,如果有更好的方式,請務(wù)必賜教。java代碼如下:

var searchQuery = new NativeSearchQueryBuilder()
                .withIndices("torrent_info").withTypes("common")
                .withQuery(QueryBuilders.multiMatchQuery(param.getKeyword(), "name", "files.path"))
                .withHighlightFields(new HighlightBuilder.Field("name").preTags("<strong>").postTags("</strong>").noMatchSize(150).numOfFragments(1), new HighlightBuilder.Field("files.path").preTags("<strong>").postTags("</strong>").noMatchSize(150).numOfFragments(3))
                .withPageable(PageRequest.of(param.getPageNum(), param.getPageSize(), sort))
                .build();
var torrentInfoPage = elasticsearchTemplate.queryForPage(searchQuery, TorrentInfoDo.class, new SearchResultMapper() {
            @SuppressWarnings("unchecked")
            @Override
            public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
                if (searchResponse.getHits().getHits().length <= 0) {
                    return null;
                }
                var chunk = new ArrayList<>();

                for (var searchHit : searchResponse.getHits()) {
                    // 設(shè)置info部分
                    var torrentInfo = new TorrentInfoDo();
                    torrentInfo.setId(searchHit.getId());
                    torrentInfo.setName((String) searchHit.getSourceAsMap().get("name"));
                    torrentInfo.setLength(Long.parseLong(searchHit.getSourceAsMap().get("length").toString()));
                    torrentInfo.setCreate_time(Long.parseLong(searchHit.getSourceAsMap().get("create_time").toString()));
                    torrentInfo.setPopularity((Integer) searchHit.getSourceAsMap().get("popularity"));
                    // ArrayList<Map>->Map->FileList->List<FileList>
                    var resList = ((ArrayList<Map>) searchHit.getSourceAsMap().get("files"));
                    var fileList = new ArrayList<FileList>();
                    for (var map : resList) {
                        FileList file = new FileList();
                        file.setPath((String) map.get("path"));
                        file.setLength(Long.parseLong(map.get("length").toString()));
                        fileList.add(file);
                    }
                    torrentInfo.setFiles(fileList);
                    // 設(shè)置highlight部分
                    // 種子名稱highlight(一般只有一個)
                    var nameHighlight = searchHit.getHighlightFields().get("name").getFragments()[0].toString();
                    // path highlight列表
                    var pathHighlight = getFileListFromHighLightFields(searchHit.getHighlightFields().get("files.path").fragments(), fileList);
                    torrentInfo.setNameHighLight(nameHighlight);
                    torrentInfo.setPathHighlight(pathHighlight);
                    chunk.add(torrentInfo);
                }
                if (chunk.size() > 0) {
                    // 不設(shè)置total返回不了正確的page結(jié)果
                    return new AggregatedPageImpl<>((List<T>) chunk, pageable, searchResponse.getHits().getTotalHits());
                }
                return null;
            }
        });

關(guān)于elasticsearch:

種子搜索不需要多高的實(shí)時性,一臺服務(wù)器也不需要副本,因此,index的設(shè)置都是這樣:

{
    "settings": {
        "number_of_shards": 2,
        "number_of_replicas": 0,
        "refresh_interval": "90s"
    }
}

jvm配置了8G內(nèi)存,G1GC,另外還禁了swapping:

## IMPORTANT: JVM heap size 
-Xms8g
-Xmx8g
## GC configuration 
-XX:+UseG1GC
-XX:MaxGCPauseMillis=50

運(yùn)行得怎么樣?

由于搜索比較復(fù)雜,平均搜索時間1s左右,搜索命中上百萬數(shù)據(jù)時會大于2s。

下面是cloudfare的統(tǒng)計:
用elasticsearch和nuxtjs搭建bt搜索引擎

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI