異構(gòu)數(shù)據(jù)半小時(shí)實(shí)現(xiàn)搜索功能,一個(gè)系統(tǒng)搞定
背景
對(duì)于閑魚(yú)這種處于高速增長(zhǎng)期的部門來(lái)說(shuō),業(yè)務(wù)場(chǎng)景在快速膨脹,越來(lái)越多的業(yè)務(wù)數(shù)據(jù)對(duì)搜索能力有訴求。如果按照常規(guī)的方式為各個(gè)業(yè)務(wù)搭建獨(dú)立搜索引擎服務(wù),那么開(kāi)發(fā)和維護(hù)的時(shí)間成本將是非常巨大的。能否只用一套搜索引擎系統(tǒng)支撐不同業(yè)務(wù)場(chǎng)景產(chǎn)出的數(shù)據(jù)呢?不同場(chǎng)景的異構(gòu)數(shù)據(jù)如何在一套引擎中兼容呢?閑魚(yú)從實(shí)際的業(yè)務(wù)需求出發(fā),搭建了一套通用搜索系統(tǒng)解決這個(gè)問(wèn)題。
搜索原理簡(jiǎn)述
閑魚(yú)使用的搜索引擎是阿里巴巴的HA3引擎,配合其上層的管控系統(tǒng)Tisplus2使用??梢圆鸱譃橐韵聨讉€(gè)子系統(tǒng):
1、dump:接入搜索系統(tǒng)首先要做的就是把DB數(shù)據(jù)經(jīng)過(guò)一些業(yè)務(wù)邏輯轉(zhuǎn)換后(后面會(huì)詳細(xì)描述的merge、join流程),按照引擎BuildService能夠識(shí)別的文件格式寫入到文件系統(tǒng)或者消息隊(duì)列中供BS構(gòu)建索引使用,這個(gè)過(guò)程分為全量與增量?jī)煞N。
2、BuildService(簡(jiǎn)稱BS):將dump產(chǎn)出的數(shù)據(jù)構(gòu)建成索引文件。Searcher機(jī)器加載了BS產(chǎn)出的索引文件后才能提供倒排、正排、summary的查詢服務(wù)。
3、搜索業(yè)務(wù)網(wǎng)關(guān):業(yè)務(wù)層封裝統(tǒng)一服務(wù)接口,對(duì)業(yè)務(wù)接入方屏蔽搜索系統(tǒng)底層細(xì)節(jié)。
4、Search Planner(簡(jiǎn)稱SP):組合搜索中臺(tái)多種能力,調(diào)用算法服務(wù)對(duì)網(wǎng)關(guān)傳入的查詢串進(jìn)行改寫、類目預(yù)測(cè)、算分,實(shí)現(xiàn)多路召回、分層查詢、翻頁(yè)去重等功能,對(duì)QRS返回的結(jié)果進(jìn)行包裝返回。
5、引擎在線服務(wù):分為QRS與Searcher兩種角色。SP的查詢請(qǐng)求發(fā)送給QRS,QRS將請(qǐng)求轉(zhuǎn)發(fā)到多臺(tái)searcher機(jī)器上然后收集searcher返回的結(jié)果進(jìn)行合并、算分、排序、返回。
整個(gè)搜索系統(tǒng)的簡(jiǎn)化版結(jié)構(gòu)圖如下:

為每個(gè)業(yè)務(wù)場(chǎng)景從頭搭建一套搜索系統(tǒng)是有一定復(fù)雜度的,而且需要花費(fèi)較多時(shí)間。我們希望提供一套通用搜索系統(tǒng),當(dāng)新的業(yè)務(wù)數(shù)據(jù)接入搜索能力時(shí),不需要業(yè)務(wù)開(kāi)發(fā)同學(xué)精通搜索系統(tǒng)原理,只要在我們的系統(tǒng)中注冊(cè)哪些數(shù)據(jù)需要被搜索,就可以完成搜索能力的自主接入,幾乎無(wú)需開(kāi)發(fā),真正實(shí)現(xiàn)十分鐘快速接入搜索。多個(gè)業(yè)務(wù)的數(shù)據(jù)共存在一套搜索引擎服務(wù)中,各業(yè)務(wù)數(shù)據(jù)相互隔離互不影響。
這里涉及到兩個(gè)問(wèn)題:
①如何把異構(gòu)數(shù)據(jù)從不同的業(yè)務(wù)db寫入同一個(gè)引擎構(gòu)建索引,且寫入過(guò)程完全自動(dòng)化、透明化,無(wú)需業(yè)務(wù)接入方參與開(kāi)發(fā);
②如何實(shí)現(xiàn)不同業(yè)務(wù)場(chǎng)景的開(kāi)發(fā)同學(xué)在使用搜索召回的過(guò)程中不感知到其他業(yè)務(wù)數(shù)據(jù)的存在,就像在使用一套為他的業(yè)務(wù)單獨(dú)搭建的引擎服務(wù)一樣方便。
我們的解法
針對(duì)上面遇到的2個(gè)問(wèn)題,我們的解決思路是提前構(gòu)建一套通用搜索系統(tǒng),把dump、bs、search、Search Planner、網(wǎng)關(guān)層的基本能力都提前實(shí)現(xiàn),業(yè)務(wù)在調(diào)用服務(wù)時(shí)通過(guò)設(shè)定可選的入?yún)?lái)選擇自己需要的能力(比如關(guān)鍵詞改寫、類目預(yù)測(cè)、pvlog打印、分層召回等)。通過(guò)一個(gè)中間層把dump流程自動(dòng)化,并對(duì)dump、search過(guò)程進(jìn)行字段翻譯、結(jié)果包裝。
搜索引擎基本服務(wù)的搭建過(guò)程與常規(guī)方式無(wú)大的差異,這里不詳細(xì)描述。接下來(lái)我詳細(xì)介紹一下此方案實(shí)施過(guò)程中拆分出的4個(gè)技術(shù)要點(diǎn):①通用搜索預(yù)留表;②元數(shù)據(jù)注冊(cè)中心;③兩層dump;④在線查詢服務(wù)。
1、通用搜索預(yù)留表
常規(guī)情況下,我們會(huì)把dump產(chǎn)出的大寬表字段命名為itemId、title、price、userId等具有明確語(yǔ)義的字段名。但是如果要實(shí)現(xiàn)多場(chǎng)景共用一套引擎就不能這么做了,因?yàn)椴⒉皇撬袌?chǎng)景數(shù)據(jù)都有itemId、title、price字段,也可能某個(gè)場(chǎng)景中需要接入color字段但是我們的引擎中沒(méi)有定義這個(gè)字段,導(dǎo)致無(wú)法支持這個(gè)業(yè)務(wù)場(chǎng)景。
既然問(wèn)題的關(guān)鍵是字段定義有語(yǔ)義,那么我們解決這個(gè)問(wèn)題的思路就是讓已引擎中的所有字段都完全無(wú)語(yǔ)義,只有類型信息。我們按照下圖的方式預(yù)定義2個(gè)維度,每個(gè)維度下各2張Mysql表和2張ODPS表(這種定義已經(jīng)可以覆蓋絕大多數(shù)場(chǎng)景了),稱為通用搜索預(yù)留表。

為每張預(yù)留表預(yù)留各種類型的字段,按照所處維度、表在維度內(nèi)位置、字段類型進(jìn)行字段命名:
1)將第一個(gè)維度中第一張預(yù)留表表的字段命名為dima_pk、dima_a_int_r1、dima_a_text_multilevel_r1、dima_dimb_joinkey等;
2)將第一個(gè)維度中第二張預(yù)留表表命名為dima_b_inner_mergekey、dima_b_int_r1、dima_b_int_r2、dima_b_long_r1等;
3)將第二個(gè)維度中第一張預(yù)留表表字段命名為dimb_pk、dimb_a_int_r1等;
4)將第二個(gè)維度中第二張預(yù)留表表命名為dimb_b_inner_mergekey、dimb_b_int_r1、dimb_b_long_r1等;
然后將預(yù)留表按照上圖的結(jié)構(gòu),與引擎原生的dump系統(tǒng)進(jìn)行對(duì)接,并配置索引構(gòu)建信息。當(dāng)這套引擎服務(wù)搭建完成后,如果直接往通用搜索預(yù)留表中接入幾條數(shù)據(jù),就已經(jīng)可以從引擎在線查詢接口中查到數(shù)據(jù)了。不過(guò)這套搜索系統(tǒng)對(duì)業(yè)務(wù)開(kāi)發(fā)同學(xué)來(lái)說(shuō)是不可用的,因?yàn)闃I(yè)務(wù)源表結(jié)果與我們的預(yù)留表結(jié)構(gòu)完全不同,業(yè)務(wù)同學(xué)很難把數(shù)據(jù)從源表按照我們預(yù)定義的格式全部遷移到通用搜索預(yù)留表,且查詢時(shí)用“dima_a_text_multilevel_r1='iPhone6S'”這種無(wú)語(yǔ)義的方式做查詢也是業(yè)務(wù)同學(xué)無(wú)法接受的。接下來(lái)我們就來(lái)解決這些問(wèn)題。
2、元數(shù)據(jù)注冊(cè)中心
我們?cè)O(shè)計(jì)了一個(gè)元數(shù)據(jù)注冊(cè)中心,當(dāng)一個(gè)新業(yè)務(wù)需要接入搜索能力時(shí),只需要在注冊(cè)中心填寫業(yè)務(wù)相關(guān)注冊(cè)信息(包括業(yè)務(wù)場(chǎng)景標(biāo)簽,需要接入搜索能力的數(shù)據(jù)庫(kù)、表名、字段等基本信息),系統(tǒng)會(huì)分配一個(gè)業(yè)務(wù)唯一識(shí)別碼,這個(gè)識(shí)別碼會(huì)作為dump、bs、查詢流程中實(shí)現(xiàn)多業(yè)務(wù)隔離的最重要標(biāo)識(shí)。
元數(shù)據(jù)注冊(cè)表結(jié)構(gòu):

元數(shù)據(jù)注冊(cè)中心以WEB界面形式提供新業(yè)務(wù)注冊(cè)的能力,用戶在填寫業(yè)務(wù)的庫(kù)名,會(huì)通過(guò)中間件自動(dòng)拉取到包含的所有表名。用戶從中選擇自己需要接入的表名,界面上列出此表下的全部字段,以及系統(tǒng)預(yù)設(shè)的全部通用搜索預(yù)留表字段。用戶在源表字段與預(yù)留表字段之間用鼠標(biāo)建立連線,完成后點(diǎn)擊提交,系統(tǒng)將對(duì)用戶建立的映射關(guān)系進(jìn)行各項(xiàng)合法性檢查,檢查通過(guò)后按照上面的元數(shù)據(jù)注冊(cè)表結(jié)構(gòu)寫入DB。這份注冊(cè)數(shù)據(jù)以后將在dump、查詢等多個(gè)環(huán)節(jié)用到。
3、兩層dump
每一張業(yè)務(wù)源表的語(yǔ)義一般比較單一,多張表組合在一起才能夠形成一個(gè)業(yè)務(wù)場(chǎng)景的全貌。比如:
1)商品基本信息表中會(huì)存儲(chǔ)商品id、標(biāo)題、描述、圖片、賣家id等信息;
2)商品擴(kuò)展信息表中會(huì)以商品id為主鍵,存儲(chǔ)商品擴(kuò)展信息,如sku信息、擴(kuò)展標(biāo)簽信息等;
3)賣家基本信息表中會(huì)以用戶id為主鍵,存儲(chǔ)用戶的昵稱、頭像等基本信息;
4)賣家信用信息表中會(huì)以用戶id為主鍵,存儲(chǔ)用戶的芝麻信用等級(jí)。
在一次典型的搜索請(qǐng)求場(chǎng)景中,用戶以“iPhone6S”進(jìn)行搜索,在搜索結(jié)果中用戶除了希望看到商品基本信息如標(biāo)題、描述、圖片等,還希望看到存儲(chǔ)在擴(kuò)展表中的sku、擴(kuò)展標(biāo)簽等擴(kuò)展信息,以及賣家的昵稱、頭像、信用等級(jí)等用戶維度信息。如何實(shí)現(xiàn)在一次召回過(guò)程把分散存儲(chǔ)在多張表中的與同一個(gè)商品相關(guān)的信息都返回呢?這就需要在dump過(guò)程中把多表數(shù)據(jù)按照一定的方式組織起來(lái),拼裝成最終希望的寬表格式,再寫入持久化存儲(chǔ)供引擎構(gòu)建索引。
我們?cè)赿ump過(guò)程中,把與此業(yè)務(wù)場(chǎng)景相關(guān)的多張表按照主鍵做merge和join。同一維度內(nèi)的多張表按照主鍵拼成大寬表的過(guò)程成為merge,比如1)和2)之間就是按照商品id做merge,結(jié)果記為M1;3)和4)之間就是按照用戶id做merge,結(jié)果記為M2。結(jié)果M1中有一列數(shù)據(jù)是賣家的用戶id,而M2的主鍵就是用戶id,將M1和M2根據(jù)用戶id做join,就得到了最終的大寬表,寬表中的任何一條數(shù)據(jù)都包含了1)2)3)4)中的完整場(chǎng)景信息。
在通用搜索預(yù)留表構(gòu)建過(guò)程中,我們已經(jīng)按照dima_pk+ima_b_inner_mergekey和dimb_pk+dimb_b_inner_mergekey的方式做維度內(nèi)merge,按照dima_pk+dimb_pk的方式做維度間join的方式完成了預(yù)留表與BuilderService的對(duì)接。只要業(yè)務(wù)同學(xué)把源表數(shù)據(jù)正確遷移到預(yù)留表中,就可以實(shí)現(xiàn)上面描述的復(fù)雜dump流程。數(shù)據(jù)遷移既要保證源表的全部數(shù)據(jù)被遷移,也要保證線上實(shí)時(shí)增量數(shù)據(jù)被遷移,而且遷移過(guò)程中需要根據(jù)元數(shù)據(jù)注冊(cè)中心的字段映射信息進(jìn)行轉(zhuǎn)換,這個(gè)流程還是比較復(fù)雜的,如何自動(dòng)化實(shí)現(xiàn)這部分工作呢?
我們的實(shí)現(xiàn)方式是基于阿里巴巴內(nèi)部的中間件平臺(tái)“精衛(wèi)”做二次開(kāi)發(fā),編寫自主消費(fèi)tar包上傳到精衛(wèi)平臺(tái)運(yùn)行,根據(jù)各業(yè)務(wù)的注冊(cè)信息完成適用于各業(yè)務(wù)的遷移任務(wù),這部分工作由我們?cè)陂_(kāi)發(fā)通用搜索系統(tǒng)時(shí)完成,對(duì)各業(yè)務(wù)接入同學(xué)完全透明。
精衛(wèi)平臺(tái)支持全量遷移任務(wù)和增量遷移任務(wù),簡(jiǎn)單的理解全量遷移任務(wù)就是循環(huán)對(duì)源表執(zhí)行“select * from table_xxx where id>m and id比如一個(gè)業(yè)務(wù)開(kāi)發(fā)同學(xué)需要為小區(qū)POI數(shù)據(jù)接入搜索能力,他在注冊(cè)中心注冊(cè)這個(gè)業(yè)務(wù),在mapping_info中聲明需要把源表的poi_id映射為dima_pk,把源表的poi_name映射為dima_a_text_r1,環(huán)境為預(yù)發(fā)環(huán)境。配置完成后,系統(tǒng)會(huì)自動(dòng)分配一個(gè)biz_code如1001。當(dāng)精衛(wèi)任務(wù)啟動(dòng)時(shí),我們上傳到精衛(wèi)的自主消費(fèi)代碼會(huì)把從源表拿到的poi_id為123123123的數(shù)據(jù)轉(zhuǎn)換為主鍵為“1001_0_123123123”的數(shù)據(jù)寫入通用搜索預(yù)留表,其中1001代表業(yè)務(wù)唯一識(shí)別碼,0代表預(yù)發(fā)環(huán)境,123123123代表原始業(yè)務(wù)主鍵。

如此一來(lái)就實(shí)現(xiàn)了用戶只需一次填寫,就自動(dòng)化完成數(shù)據(jù)dump的工作。
4、在線查詢服務(wù)
既然dump產(chǎn)出數(shù)據(jù)的字段是無(wú)語(yǔ)義的,那么相應(yīng)的BuildService構(gòu)建處端索引數(shù)據(jù)各字段也是無(wú)語(yǔ)義的。
這里看起來(lái)通過(guò)無(wú)語(yǔ)義的定義方式支持了將多場(chǎng)景異構(gòu)數(shù)據(jù)寫入同一個(gè)引擎服務(wù),但是對(duì)業(yè)務(wù)開(kāi)發(fā)同學(xué)來(lái)說(shuō)太不友好了。他們?cè)跇I(yè)務(wù)開(kāi)發(fā)中調(diào)用搜索服務(wù)時(shí),期望的方式是自然的業(yè)務(wù)語(yǔ)義調(diào)用,如下面的代碼片段:
- param.setTitle("iPhone6S");
- param.setSellerId(1234567L);
- result = searchService.doSearch(param);
但是現(xiàn)在字段沒(méi)了語(yǔ)義,他們開(kāi)發(fā)的復(fù)雜度大大提升,甚至?xí)r間一長(zhǎng)會(huì)陷入難以維護(hù)的境地,因?yàn)闃I(yè)務(wù)代碼寫完1個(gè)月后沒(méi)人會(huì)再記得代碼中的“param.setDimaALongR1(1234567L)”是什么意思,這是按照用戶id還是商品id查詢?
雖然底層我們是將多個(gè)業(yè)務(wù)的數(shù)據(jù)放在一個(gè)引擎服務(wù)中,但是我們希望提供給業(yè)務(wù)開(kāi)發(fā)同學(xué)(也就是我們這套系統(tǒng)的用戶)的在線查詢服務(wù)與獨(dú)立搭建一套引擎的體驗(yàn)是一樣的。所以,這里就需要有一個(gè)翻譯層,通用搜索系統(tǒng)接收到的查詢請(qǐng)求是“title=iPhone6S”,我們需要根據(jù)元數(shù)據(jù)注冊(cè)中心的映射關(guān)系自動(dòng)翻譯成“dima_a_text_multilevel_r1=iPhone6S”后再向引擎發(fā)起搜索請(qǐng)求,并把引擎返回的數(shù)據(jù)DO中無(wú)語(yǔ)義字段翻譯成源表的有語(yǔ)義字段。

可以看到,通過(guò)我們提供的搜索網(wǎng)關(guān)二方包,業(yè)務(wù)同學(xué)可以按照有語(yǔ)義的方式設(shè)置查詢條件“param.setTitle("iPhone6S")”,同時(shí)自動(dòng)化把引擎返回的無(wú)語(yǔ)義字段進(jìn)行包裝成為有語(yǔ)義的字段。業(yè)務(wù)同學(xué)完全覺(jué)察不到中間的轉(zhuǎn)換過(guò)程,對(duì)他來(lái)說(shuō)就像在使用一個(gè)為他單獨(dú)搭建的搜索引擎服務(wù)一樣。
每個(gè)業(yè)務(wù)接入方的源表字段定義都不同,只寫一套搜索網(wǎng)關(guān)代碼肯定無(wú)法實(shí)現(xiàn)上面的能力。我們的方案是,當(dāng)用戶在元數(shù)據(jù)注冊(cè)中心曾經(jīng)接入一個(gè)新業(yè)務(wù)后,后臺(tái)自動(dòng)化生成生成為業(yè)務(wù)定制的二方包代碼,其中包含了查詢?nèi)雲(yún)?、返回DO、查詢服務(wù)接口。
還是以poi數(shù)據(jù)接入為例,poi業(yè)務(wù)域的開(kāi)發(fā)同學(xué)在元數(shù)據(jù)注冊(cè)中心說(shuō)明了他需要按照poi_name做文本模糊匹配,需要根據(jù)poi_code做包含查詢、不包含的精確查詢。根據(jù)此登記信息,我們?yōu)橛脩糇詣?dòng)生成poi業(yè)務(wù)場(chǎng)景專用的查詢服務(wù)入?yún)?,每個(gè)入?yún)⒍际且欢ǖ囊?guī)則拼接而成,網(wǎng)關(guān)在線服務(wù)拿到此參數(shù)后可以根據(jù)命名規(guī)則翻譯成具體的查詢串。參數(shù)命名規(guī)則如下圖:

入?yún)emo代碼如下:
- public class UnisearchBiz1001SearchParam extends IdleUnisearchBaseSearchParams {
- private Set<Long> unisearch_includeCollection_prefix_poiCode;
- private Set<Long> unisearch_excludeCollection_prefix_poiCode;
- private String unisearch_keywords_poiName;
- }
用戶通過(guò)在線查詢服務(wù)把此查詢條件傳入時(shí),查詢服務(wù)檢測(cè)到入?yún)⑹荌dleUnisearchBaseSearchParams的之類,會(huì)根據(jù)命名規(guī)則使用反射機(jī)制判定unisearch_includeCollection_prefix_poiCode是需要對(duì)業(yè)務(wù)源表的poiCode字段做include(包含)查詢,然后從元數(shù)據(jù)注冊(cè)中心的映射關(guān)系數(shù)據(jù)取出poiCode對(duì)應(yīng)的預(yù)留表字段名為dima_a_long_r1,構(gòu)造Search Planner查詢串,執(zhí)行后續(xù)查詢動(dòng)作。
當(dāng)引擎返回查詢結(jié)果后,網(wǎng)關(guān)查詢服務(wù)再次根據(jù)元數(shù)據(jù)注冊(cè)信息,利用反射對(duì)引擎結(jié)果DO進(jìn)行翻譯轉(zhuǎn)換,包裝成下面所示的poi業(yè)務(wù)專用DO后返回給業(yè)務(wù)開(kāi)發(fā)同學(xué)。
- public class UnisearchBiz1001SearchResultDo extends IdleUnisearchBaseSearchResultDo {
- private Long poiId;
- private Long poiCode;
- private String poiName;
- }
通用搜索預(yù)留表一共有8張,全部字段加起來(lái)是比較多的。如果把字段全部召回,實(shí)際上大部分字段都是業(yè)務(wù)沒(méi)有進(jìn)行注冊(cè)的空字段,返回?cái)?shù)據(jù)會(huì)比實(shí)際需要的數(shù)據(jù)大小膨脹幾十倍,網(wǎng)絡(luò)傳輸開(kāi)銷、大量空字段反序列化開(kāi)銷、DO字段轉(zhuǎn)換開(kāi)銷會(huì)導(dǎo)致在線查詢服務(wù)的RT很高。解決此問(wèn)題比較簡(jiǎn)單,我們把整個(gè)在線召回流程定義為兩個(gè)階段,第一個(gè)階段只根據(jù)用戶的查詢條件在引擎中召回符合要求的數(shù)據(jù)主鍵rawpk;第二階段根據(jù)此rawpk列表去獲取對(duì)應(yīng)數(shù)據(jù)的summary(即所有字段的信息)時(shí),利用引擎支持的dl語(yǔ)法,要求二階段僅返回用戶注冊(cè)過(guò)的預(yù)留字段即可。當(dāng)然,這些工作也由我們?cè)谕ㄓ盟阉飨到y(tǒng)的網(wǎng)關(guān)代碼中提前實(shí)現(xiàn)了,對(duì)各業(yè)務(wù)接入同學(xué)透明。
增量問(wèn)題的特殊解法
到現(xiàn)在為止一切看起來(lái)都很完美,貌似我們已經(jīng)用這套系統(tǒng)完美解決了數(shù)據(jù)導(dǎo)入、轉(zhuǎn)換、bs、查詢等一系列工作的自動(dòng)化包裝,業(yè)務(wù)同學(xué)需要做的僅僅是來(lái)我們的業(yè)務(wù)注冊(cè)中心界面上登記一下而已。不過(guò)實(shí)際上在表面之下還隱藏著一個(gè)較嚴(yán)峻的問(wèn)題,就是大增量的問(wèn)題。
由于與BuildeService直接對(duì)接的是結(jié)構(gòu)已經(jīng)固定的通用搜索預(yù)留表,也就意味著原生的dump層數(shù)據(jù)源結(jié)構(gòu)是不可能變化的,唯一能變化的是從業(yè)務(wù)源表經(jīng)過(guò)精衛(wèi)系統(tǒng)寫入通用搜索預(yù)留表的數(shù)據(jù)。當(dāng)上游有一個(gè)新業(yè)務(wù)接入進(jìn)來(lái)時(shí),如果它的源表數(shù)據(jù)量達(dá)到了十億級(jí)別,按照目前精衛(wèi)能夠達(dá)到的遷移速度,也就意味著通用搜索預(yù)留表的更新TPS能夠達(dá)到5萬(wàn)的級(jí)別,而這每秒5萬(wàn)條數(shù)據(jù)更新的壓力就會(huì)直接打在實(shí)時(shí)BS系統(tǒng)上,即引擎需要每秒更新5萬(wàn)條doc數(shù)據(jù)才能保證搜索結(jié)果與源表數(shù)據(jù)的一致性。而搜索引擎的實(shí)時(shí)BS能力依賴于實(shí)時(shí)內(nèi)存的容量,這么大的增量TPS會(huì)在短時(shí)間內(nèi)打滿實(shí)時(shí)內(nèi)存,導(dǎo)致源表后續(xù)的更新數(shù)據(jù)無(wú)法實(shí)時(shí)被BS構(gòu)建成索引,那么搜索系統(tǒng)就無(wú)法搜索到新的業(yè)務(wù)數(shù)據(jù)(包括新增、更新、刪除的數(shù)據(jù)),稱為增量延遲問(wèn)題。
多個(gè)業(yè)務(wù)共享這套引擎服務(wù),線上已經(jīng)在提供搜索服務(wù)的業(yè)務(wù)無(wú)法接受增量延遲;而對(duì)本次新接入的業(yè)務(wù)來(lái)說(shuō),在第一次接入時(shí)把數(shù)據(jù)往通用預(yù)留表同步的這段時(shí)間內(nèi),數(shù)據(jù)搜索不到是完全可以接受的。因此,我們想出一個(gè)辦法實(shí)現(xiàn)了線上存量業(yè)務(wù)的增量數(shù)據(jù)正常實(shí)時(shí)進(jìn)引擎,而新業(yè)務(wù)的全量遷移數(shù)據(jù)引發(fā)的增量不實(shí)時(shí)進(jìn)引擎。具體實(shí)現(xiàn)分為以下幾個(gè)步驟:
1、通用搜索預(yù)留表按照db表的創(chuàng)建規(guī)范,都會(huì)有一個(gè)gmt_modofied字段,類型為datetime。當(dāng)預(yù)留表中的數(shù)據(jù)發(fā)生任何變化(增、刪、改)時(shí),gmt_modofied字段都會(huì)更新為本次操作的時(shí)間戳。這個(gè)邏輯在精衛(wèi)遷移任務(wù)的DAO層實(shí)現(xiàn)。
2、為通用搜索預(yù)留表的每一張表額外增加一個(gè)datetime類型字段,命名為gmt_drop_inc_tag。精衛(wèi)全量任務(wù)為新業(yè)務(wù)導(dǎo)數(shù)據(jù)時(shí),我們?cè)诰l(wèi)任務(wù)啟動(dòng)參數(shù)中帶上“drop_inc_tag=true”的標(biāo)識(shí),相應(yīng)地我們?cè)诰l(wèi)自主消費(fèi)代碼中識(shí)別到這個(gè)標(biāo)識(shí)后,會(huì)在完成數(shù)據(jù)轉(zhuǎn)換生成的DAO層入?yún)O中把gmt_drop_inc_tag字段賦值為gmt_modofied一樣的值,然后寫入DB。而非新業(yè)務(wù)全量和增量精衛(wèi)任務(wù)的啟動(dòng)參數(shù)中無(wú)“drop_inc_tag=true”的標(biāo)識(shí),則其他業(yè)務(wù)的增量精衛(wèi)任務(wù)寫入DB的記錄中只會(huì)更新gmt_modofied而不會(huì)更新gmt_drop_inc_tag字段。
3、在引擎原生dump層,我們?cè)诿恳粡埻ㄓ盟阉黝A(yù)留表與后續(xù)merge節(jié)點(diǎn)之間增加一層udtf邏輯代碼。這里的udtf代碼是dump層對(duì)外開(kāi)放的一個(gè)口子,允許引擎接入方在dump流程上對(duì)數(shù)據(jù)做一些特殊處理,每一條上游的數(shù)據(jù)都會(huì)經(jīng)過(guò)udtf的邏輯處理后再輸出到下游進(jìn)行merge、join、輸出給BS系統(tǒng)。我們?cè)谶@里實(shí)現(xiàn)的邏輯是,如果識(shí)別到當(dāng)前是全量dump流程,則把當(dāng)前流入數(shù)據(jù)的gmt_drop_inc_tag置為空,然后向下游輸出;若識(shí)別到當(dāng)前是增量dump流程,則檢查當(dāng)前流入的數(shù)據(jù)其gmt_modofied字段與gmt_drop_inc_tag字段是否相同,若兩字段相同則對(duì)此數(shù)據(jù)執(zhí)行drop邏輯,若兩字段不同則把當(dāng)前流入數(shù)據(jù)的gmt_drop_inc_tag置為空后向下游輸出。所有被執(zhí)行了drop邏輯的數(shù)據(jù)都會(huì)被dump系統(tǒng)丟棄,不會(huì)輸出到最終產(chǎn)出的數(shù)據(jù)文件中。
如此一來(lái),老業(yè)務(wù)的增量數(shù)據(jù)都任然正常經(jīng)過(guò)dump流程后通過(guò)BS(BuildServcie)系統(tǒng)實(shí)時(shí)反映到搜索在線服務(wù)中。而本次新接入業(yè)務(wù)的全量遷移數(shù)據(jù)都只是從業(yè)務(wù)源表遷移到了搜索預(yù)留表中,BS系統(tǒng)完全感知不到這批數(shù)據(jù)的存在。待新業(yè)務(wù)的數(shù)據(jù)已經(jīng)全部從源表遷移到預(yù)留表之后,我們對(duì)引擎服務(wù)觸發(fā)一次大全量流程,即先以全量dump的方式把通用搜索預(yù)留表的數(shù)據(jù)全部重新走一次dump邏輯,產(chǎn)出完整的HDFS數(shù)據(jù),然后離線BS系統(tǒng)批量對(duì)此HDFS數(shù)據(jù)構(gòu)建索引,然后加載到searcher機(jī)器提供在線服務(wù)。隨后,對(duì)新業(yè)務(wù)開(kāi)啟精衛(wèi)增量遷移任務(wù),保證業(yè)務(wù)源表的變更實(shí)時(shí)反映到引擎中。

效果
閑魚(yú)這套通用搜索系統(tǒng)目前已經(jīng)在線上為3個(gè)業(yè)務(wù)提供服務(wù),每個(gè)新業(yè)務(wù)都能在10~30分鐘內(nèi)完成接入。而在有此系統(tǒng)之前,一個(gè)業(yè)務(wù)方如果想接入搜索能力,需要向團(tuán)隊(duì)中精通搜索底層原理的搜索業(yè)務(wù)owner提開(kāi)發(fā)需求,等待一周左右開(kāi)發(fā)排期,待搜索owner完成一套引擎服務(wù)搭建后,業(yè)務(wù)同學(xué)后才能進(jìn)入業(yè)務(wù)開(kāi)發(fā)階段。 我們用此系統(tǒng)消除了搜索owner的單點(diǎn)阻塞問(wèn)題,實(shí)現(xiàn)了用自動(dòng)化技術(shù)解放生產(chǎn)力。
展望
閑魚(yú)還將繼續(xù)在自動(dòng)化提效方面進(jìn)行更多探索,把開(kāi)發(fā)同學(xué)從繁重的重復(fù)性工作中解放出來(lái),將時(shí)間投入到更具有創(chuàng)造性意義的工作中。閑魚(yú)今年有更多有深度有挑戰(zhàn)的項(xiàng)目在進(jìn)行中,期待您的加入,與我們共同創(chuàng)造奇跡。