據(jù)清洗實(shí)戰(zhàn):構(gòu)建文本去重引擎的完整方案)
做爬蟲時(shí)間久了你會(huì)發(fā)現(xiàn)真正麻煩的往往不是“怎么把數(shù)據(jù)抓下來”而是“抓到之后怎么處理”。最常見的一個(gè)污染源就是重復(fù)文本同一個(gè)新聞被幾十個(gè)網(wǎng)站轉(zhuǎn)載同一篇商品描述在不同店鋪反復(fù)出現(xiàn)同一條公告被改了標(biāo)題又發(fā)一遍。如果你把這些數(shù)據(jù)直接喂給下游的搜索、推薦、輿情分析結(jié)果就是一堆冗余項(xiàng)在計(jì)算里反復(fù)出現(xiàn)指標(biāo)全被稀釋模型訓(xùn)練還會(huì)被重復(fù)樣本帶偏。我早期做爬蟲項(xiàng)目時(shí)去重就用一個(gè)set裝URL后來發(fā)現(xiàn)完全不夠用。很多網(wǎng)站會(huì)把同一篇文章掛在不同的路徑下URL對(duì)不上正文卻一模一樣還有更狠的把正文改幾個(gè)字、插一段廣告、換一下段落順序就當(dāng)成新文章發(fā)布。這類“偽新內(nèi)容”才是爬蟲數(shù)據(jù)質(zhì)量的頭號(hào)殺手。這篇文章就基于我反復(fù)折騰出來的一套方案詳細(xì)拆解怎么構(gòu)建一個(gè)文本資源去重引擎從精確去重一路做到語(yǔ)義級(jí)去重全是可直接落地的工程實(shí)踐。1. 先盤清楚需求精確去重和語(yǔ)義去重解決的其實(shí)是兩個(gè)不同的問題1.1 一個(gè)典型場(chǎng)景新聞聚合爬蟲里發(fā)生了什么假設(shè)你在做一個(gè)新聞聚合系統(tǒng)每天要從幾百個(gè)站點(diǎn)抓取幾萬篇文章??雌饋砻科恼露加凶约旱腢RL、標(biāo)題、發(fā)布時(shí)間數(shù)據(jù)量很可觀。但只要你抽樣比對(duì)正文就會(huì)發(fā)現(xiàn)重復(fù)率遠(yuǎn)超預(yù)期。常見的情況有這么幾類同一篇通稿被A、B、C三家網(wǎng)站原封不動(dòng)轉(zhuǎn)載連標(biāo)點(diǎn)符號(hào)都沒變。某網(wǎng)站轉(zhuǎn)載時(shí)加了一行“本文來源XXX版權(quán)歸原作者所有”。某自媒體把新聞改了標(biāo)題正文里刪掉兩段、再塞進(jìn)一段自己的評(píng)論。更惡劣的是批量洗稿同義替換、段落打亂機(jī)器生成的痕跡非常重。第一類問題用精確去重就能解決后面幾類哈希算法完全失效必須上語(yǔ)義判斷。很多初學(xué)者的誤區(qū)就是“只做了MD5就去重”或者反過來“一上來就搞Embedding”其實(shí)這兩者不是替代關(guān)系而是配合關(guān)系各管一段。1.2 精確去重與語(yǔ)義去重的邊界劃分我習(xí)慣這樣劃分兩類技術(shù)的職責(zé)精確去重負(fù)責(zé)攔截“字節(jié)級(jí)完全相同”的內(nèi)容。輸入的文本經(jīng)過規(guī)范化后通過哈?;蜻^濾器判斷是否已經(jīng)存在。它速度快、內(nèi)存占用低、容易分布式但只對(duì)完全一致或基本一致的文本有效。語(yǔ)義去重負(fù)責(zé)識(shí)別“文本不同但含義接近”的內(nèi)容。它需要把文本映射成可比較的向量或指紋再通過距離/相似度判斷是否屬于同一信息。它能攔下同義詞改寫、段落重排、插播廣告等清洗手段但計(jì)算成本高還有誤殺風(fēng)險(xiǎn)。這兩層判斷在落地時(shí)是有先后順序的。精確去重先跑一遍能把絕大多數(shù)一模一樣的重復(fù)擋在門外避免進(jìn)入高成本的語(yǔ)義計(jì)算流程語(yǔ)義去重再對(duì)剩余內(nèi)容做相似度判斷識(shí)別那些“形不同而神似”的重復(fù)項(xiàng)。整體效率要高很多。1.3 目標(biāo)與技術(shù)選型對(duì)照本文要構(gòu)建的去重引擎我用一組目標(biāo)來約束它支持千萬級(jí)文本量的單機(jī)去重內(nèi)存可控在10GB以內(nèi)。精確層平均單條判斷耗時(shí)小于1毫秒。語(yǔ)義層能處理每天幾萬條新增內(nèi)容的增量去重。對(duì)外提供統(tǒng)一的is_duplicate(text)接口上游調(diào)用方不關(guān)心內(nèi)部邏輯?;谶@些目標(biāo)精確層我用了Redis Set 布隆過濾器語(yǔ)義層用了SimHash指紋 輕量級(jí)向量雙方案。下面的章節(jié)逐個(gè)拆解。2. 精確去重引擎從哈希摘要到Redis的億級(jí)判重2.1 最簡(jiǎn)單的做法全文MD5為什么它能擋住90%重復(fù)精確去重的核心思路是對(duì)文本做摘要用摘要是否出現(xiàn)過判斷是否重復(fù)。最樸素的做法是算全文哈希import hashlib def text_digest(text: str) - str: normalized text.strip() return hashlib.sha1(normalized.encode(utf-8)).hexdigest()然后把摘要存在一個(gè)集合里新來的文本算完摘要查一下是否存在。這個(gè)方案能擋住所有“字節(jié)級(jí)相同”的重復(fù)對(duì)新聞轉(zhuǎn)載、商品描述復(fù)制這類場(chǎng)景非常有效。我在項(xiàng)目里用過SHA-1而不是MD5雖然MD5更快但SHA-1在安全性和分布均勻性上更穩(wěn)妥反正計(jì)算量差別不大。門檻在于這個(gè)方案要求文本必須“完全一致”。很多轉(zhuǎn)載網(wǎng)站會(huì)在正文前后自動(dòng)追加版權(quán)橫幅、推薦位等動(dòng)態(tài)內(nèi)容導(dǎo)致正文每次抓取都有細(xì)微差別。這時(shí)候就必須引入一個(gè)非常重要的前置步驟文本規(guī)范化。2.2 文本規(guī)范化比哈希本身更影響去重效果規(guī)范化是指在做哈希之前把同一內(nèi)容的不同表現(xiàn)形式統(tǒng)一起來。這一層做得好不好直接決定去重率。我常用的規(guī)范化步驟包括去除首尾空白字符和不可見字符。統(tǒng)一換行符為\n去除多余空行。全角英文字符和數(shù)字轉(zhuǎn)半角中文標(biāo)點(diǎn)與英文標(biāo)點(diǎn)盡量統(tǒng)一??蛇x小寫化、去除HTML標(biāo)簽殘留。一個(gè)簡(jiǎn)單的實(shí)現(xiàn)import re import unicodedata def normalize_text(text: str) - str: text unicodedata.normalize(NFKC, text) text re.sub(r\s, , text) text re.sub(r[ \t], , text) return text.strip()值得說明的是NFKC標(biāo)準(zhǔn)化會(huì)把全角字母數(shù)字轉(zhuǎn)成半角但不會(huì)過度修改中文。對(duì)中文文本來說標(biāo)點(diǎn)符號(hào)的處理需要謹(jǐn)慎不要把所有中文標(biāo)點(diǎn)都替換掉因?yàn)檫@會(huì)改變內(nèi)容的語(yǔ)義表達(dá)也會(huì)造成不同原文的碰撞。規(guī)范化規(guī)則應(yīng)該由業(yè)務(wù)方確認(rèn)后固化下來不要頻繁調(diào)整否則歷史指紋會(huì)失效。2.3 布隆過濾器用幾十MB內(nèi)存換千萬級(jí)去重能力當(dāng)數(shù)據(jù)量漲到千萬級(jí)以上直接把全部哈希值存在內(nèi)存里就有點(diǎn)吃不消了。一個(gè)SHA-1摘要40個(gè)字符存1000萬條就是400MB以上而且還要考慮Set結(jié)構(gòu)本身的開銷實(shí)際占用可能翻倍。這時(shí)候布隆過濾器是更好的選擇。布隆過濾器的原理不復(fù)雜用一個(gè)位數(shù)組和若干個(gè)哈希函數(shù)寫入時(shí)把每個(gè)哈希函數(shù)計(jì)算的位都置1查詢時(shí)檢查這些位是否全部為1。只要有任何一個(gè)位是0說明元素肯定不存在如果全部是1說明大概率存在。它用“可能誤判存在”換取了極低的內(nèi)存占用。Python實(shí)現(xiàn)選型上我建議優(yōu)先用pybloom_live或直接基于redis的bitmap實(shí)現(xiàn)避免自己造輪子。自建內(nèi)存版本可以參考from pybloom_live import BloomFilter import hashlib bf BloomFilter(capacity10_000_000, error_rate0.001) def add_bf(digest: str): bf.add(digest) def maybe_exists_bf(digest: str) - bool: return digest in bf注意布隆過濾器有一個(gè)比較麻煩的特性它不刪除元素也沒有辦法更新。如果你需要做“重新抓取后更新指紋”的場(chǎng)景必須用帶計(jì)數(shù)功能的擴(kuò)展版本或者定期重建過濾器。我在項(xiàng)目里的做法是引入版本號(hào)每天生成一個(gè)新的布隆過濾器查重時(shí)先查昨天的再查今天的歷史版本保留一周后回收。2.4 Redis版去重精確與近似的折中如果你的爬蟲系統(tǒng)本身就部署了Redis直接使用Redis的Set或HyperLogLog做去重會(huì)更省事。Set可以精確判斷元素是否存在但內(nèi)存占用較高HyperLogLog內(nèi)存占用極低但只能統(tǒng)計(jì)基數(shù)不能做“是否存在”的判斷所以實(shí)際去重場(chǎng)景很少用它。我最終的精確層方案是“Redis Set 本地布隆過濾器”組合所有新增文本的哈希先寫入本地布隆過濾器快速擋住絕大多重復(fù)。未命中過濾器的再去Redis Set里確認(rèn)是否真不存在。確認(rèn)新增后哈希寫入Redis Set和本地布隆過濾器。這樣一來Redis的請(qǐng)求量下降了好幾倍同時(shí)保證了“寧可多查一次也不能漏判”的精確性。布隆過濾器的誤判存在只影響性能不影響正確性因?yàn)檎`判后還會(huì)去Redis確認(rèn)。3. 語(yǔ)義級(jí)去重讓“改幾個(gè)字、換順序、多段插播”的重復(fù)稿也能被識(shí)別3.1 為什么哈希失效了內(nèi)容農(nóng)場(chǎng)和AI洗稿的常見手法精確去重處理不了這樣一類文本核心信息完全一致但表面文字被做了手腳。我見過的手法包括同義詞替換把“汽車”改成“車輛”“購(gòu)買”改成“購(gòu)置”。段落重排原文是1-2-3-4洗稿后變成4-1-3-2。插入噪音正文中間插入一段“更多相關(guān)資訊請(qǐng)關(guān)注XXX”之類的引導(dǎo)語(yǔ)。首段改寫開頭幾句換成自己的話后面整段復(fù)制。這些改寫后的文本哈希值完全不一樣你去重引擎直接放行但實(shí)際上它們傳達(dá)的資訊是同一個(gè)。這時(shí)候必須做語(yǔ)義級(jí)別的判斷。3.2 不急著上大模型先用SimHash做指紋語(yǔ)義去重第一步我建議先從SimHash開始。SimHash的核心思想是把文本轉(zhuǎn)換成一個(gè)64位的指紋然后用海明距離衡量?jī)蓚€(gè)文本的相似度。海明距離越小文本越相似。一般經(jīng)驗(yàn)是海明距離≤3基本可以判定為重復(fù)內(nèi)容。SimHash實(shí)現(xiàn)不復(fù)雜核心步驟是對(duì)文本分詞拿到帶權(quán)重的關(guān)鍵詞列表。每個(gè)詞做哈希得到64位二進(jìn)制串。如果是1對(duì)應(yīng)維度加權(quán)重如果是0對(duì)應(yīng)維度減權(quán)重。所有詞貢獻(xiàn)累加后正數(shù)取1負(fù)數(shù)取0得到64位指紋。用第三方庫(kù)可以直接做from simhash import Simhash hash1 Simhash(這是一條被改寫的新聞?wù)闹v述某地發(fā)生的重要事件) hash2 Simhash(這是一個(gè)被改寫的新聞?wù)闹v述某地發(fā)生的重要事件) distance hash1.distance(hash2) print(distance) # 數(shù)值越小越相似一般 3 算重復(fù)SimHash的優(yōu)點(diǎn)是速度極快、內(nèi)存占用小非常適合海量文檔的粗篩。它的缺點(diǎn)是只捕捉詞袋層面的差異對(duì)語(yǔ)義理解幾乎沒有同義詞替換會(huì)被它放過因?yàn)椤捌嚒焙汀败囕v”是兩個(gè)完全不同的詞哈希。所以SimHash適合做“低配版語(yǔ)義去重”能攔住段落重排、插播廣告這類混入方式但對(duì)真正的同義改寫比較吃力。3.3 語(yǔ)義向量方案嵌入模型余弦相似度的工程化要讓去重引擎真正理解語(yǔ)義需要把文本轉(zhuǎn)換成向量然后比較余弦相似度。我使用的是輕量級(jí)的sentence-transformers模型它在embedding句子和短文檔時(shí)效果不錯(cuò)而且能直接輸出定長(zhǎng)向量。from sentence_transformers import SentenceTransformer model SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) text1 蘋果公司發(fā)布了新款手機(jī)價(jià)格比上一代貴了不少。 text2 蘋果新機(jī)正式推出售價(jià)較前代有顯著上漲。 text3 今天天氣很好適合出門散步。 vec1 model.encode(text1) vec2 model.encode(text2) vec3 model.encode(text3) from sklearn.metrics.pairwise import cosine_similarity print(cosine_similarity([vec1], [vec2])) # 高相似度 print(cosine_similarity([vec1], [vec3])) # 低相似度實(shí)測(cè)下來paraphrase-multilingual-MiniLM-L12-v2對(duì)中文短文檔的效果不錯(cuò)單條文本向量化大約幾十毫秒速度可以接受。如果數(shù)據(jù)量特別大、硬件緊張可以退回去用SimHash先粗篩只有SimHash判定相似度較高的文本才進(jìn)入向量層二次確認(rèn)。這個(gè)“粗篩精排”的思路能大大降低向量計(jì)算的壓力。向量存儲(chǔ)方面數(shù)據(jù)量不大的時(shí)候直接用numpy數(shù)組存內(nèi)存每來一條就和歷史向量做一次余弦相似度。但數(shù)據(jù)量到十萬級(jí)以上全量遍歷會(huì)越來越慢這時(shí)要引入近似最近鄰索引。我推薦用annoy或faiss這兩者都能在犧牲極小精度的情況下做到毫秒級(jí)相似檢索。以annoy為例構(gòu)建索引和查詢都很方便from annoy import AnnoyIndex dim 384 index AnnoyIndex(dim, metricangular) # 添加向量 index.add_item(0, vec1) index.add_item(1, vec2) index.build(10) # 10棵樹樹越多精度越高、內(nèi)存越大 # 查詢相似 neighbors index.get_nns_by_vector(vec1, 10, include_distancesTrue)注意annoy的angular距離和余弦相似度是有換算關(guān)系的構(gòu)建索引時(shí)用angular查詢時(shí)返回的距離越小越相似。實(shí)際使用中g(shù)et_nns_by_vector返回的距離需要轉(zhuǎn)換成相似度來看或者直接設(shè)定一個(gè)距離閾值。3.4 閾值怎么定先算一遍真實(shí)數(shù)據(jù)的分布語(yǔ)義去重里最容易被忽視、也最容易翻車的就是相似度閾值的設(shè)置。定得太高放過洗稿定得太低誤殺大量正常文章。我強(qiáng)烈建議不要在第一天就拍腦袋定閾值而是先拿一批真實(shí)數(shù)據(jù)算一遍相似度分布。具體做法是隨機(jī)抽取1000篇最新抓取的文章兩兩配對(duì)計(jì)算相似度然后把結(jié)果按區(qū)間分布統(tǒng)計(jì)。你通常會(huì)看到兩個(gè)明顯的峰一個(gè)集中在0.95~1.0是字節(jié)級(jí)重復(fù)或輕度改寫的另一個(gè)集中在0.5~0.7是正常文章的隨機(jī)相似度。閾值可以選在兩個(gè)峰之間的低谷處。我項(xiàng)目的實(shí)測(cè)經(jīng)驗(yàn)大致如下場(chǎng)景推薦余弦相似度閾值說明新聞轉(zhuǎn)載、通稿0.92 ~ 0.95同一信息源的改寫幅度較小商品描述0.86 ~ 0.90商家經(jīng)常調(diào)整描述詞但核心信息一致用戶評(píng)論、公告0.82 ~ 0.88表達(dá)方式差異較大需要放寬需要特別提醒的是閾值不是一成不變的不同業(yè)務(wù)、不同語(yǔ)料都要單獨(dú)調(diào)。而且每當(dāng)你更換embedding模型所有歷史向量的分布都會(huì)變化必須重新評(píng)估閾值不能沿用舊值。4. 雙路引擎的架構(gòu)設(shè)計(jì)與數(shù)據(jù)流單機(jī)也能跑出穩(wěn)定效果4.1 整體流程從下載、解析到去重的完整管線把精確和語(yǔ)義兩層串起來我推薦下面這個(gè)流程原始HTML - 正文抽取 - 文本清洗與規(guī)范化 - 精確去重布隆過濾器 Redis Set - 語(yǔ)義去重SimHash粗篩 向量精排 - 寫入已去重庫(kù)每一步都有它的職責(zé)。正文抽取決定了后續(xù)文本的質(zhì)量如果這里抽到的是一堆導(dǎo)航、頁(yè)腳、廣告代碼后面所有環(huán)節(jié)都會(huì)受影響清洗和規(guī)范化讓哈希能對(duì)“同一內(nèi)容”穩(wěn)定生成同一個(gè)摘要精確去重快速攔截完全相同的文本語(yǔ)義去重處理那些“形不同而神似”的重復(fù)最后被判定為新增內(nèi)容的數(shù)據(jù)才允許入庫(kù)。4.2 精確層和語(yǔ)義層怎么配合才不浪費(fèi)算力兩層不是簡(jiǎn)單的“精確失敗再進(jìn)語(yǔ)義”還需要考慮成本和數(shù)據(jù)特點(diǎn)。我在生產(chǎn)環(huán)境里的策略是對(duì)每條新文本先規(guī)范化。精確層判斷是否“絕對(duì)重復(fù)”。是直接丟棄。精確層未命中時(shí)先做SimHash粗篩。因?yàn)镾imHash計(jì)算很快可以先把海明距離小于某個(gè)較大閾值比如≤6的候選集找出來。如果SimHash沒有找到候選直接認(rèn)定為新增不再進(jìn)入向量層。如果SimHash找到候選再把這些候選文本做向量化用余弦相似度做最終判斷。這樣做最直接的好處是真正走到向量層的數(shù)據(jù)量非常少。我跑過的項(xiàng)目里大約只有2%~5%的文本會(huì)進(jìn)入向量精排剩余的95%以上在精確層和SimHash層就能搞定整機(jī)負(fù)載低了很多。4.3 增量更新與歷史指紋管理去重引擎必須處理增量更新的問題。每天都有新增文本而歷史指紋會(huì)越來越大。如果不做管理內(nèi)存和查詢時(shí)間都會(huì)被拖垮。我采用的方案是“按天分桶”每天的精確層哈希存到獨(dú)立的Redis Key或獨(dú)立的布隆過濾器快照里。語(yǔ)義層的向量也按天寫入獨(dú)立的索引文件。查重時(shí)先查當(dāng)天桶再查前一天桶最多往前查7天。這個(gè)做法有一個(gè)業(yè)務(wù)假設(shè)絕大多數(shù)重復(fù)內(nèi)容會(huì)在發(fā)布后的48小時(shí)內(nèi)被抓到。只要重復(fù)內(nèi)容在7天之內(nèi)出現(xiàn)過就會(huì)被識(shí)別超過7天的系統(tǒng)不會(huì)太在意因?yàn)閷?duì)搜索、推薦、榜單來說一周前已經(jīng)處理過的舊文章再重復(fù)出現(xiàn)本身也基本影響不大了。如果你需要全量歷史去重那就要跑一次全量構(gòu)建而不是增量流程。增量更新還有一個(gè)好處不管是布隆過濾器還是向量索引都需要定期重建來清理增長(zhǎng)。按天分桶之后重建某一天的桶不會(huì)影響其他數(shù)據(jù)。4.4 性能指標(biāo)實(shí)測(cè)下面是我在單機(jī)環(huán)境16GB內(nèi)存、8核CPU、SSD下做過的一組實(shí)測(cè)數(shù)據(jù)供參考指標(biāo)數(shù)值備注精確層單條判斷耗時(shí)0.1ms ~ 0.5ms本地布隆 Redis確認(rèn)SimHash單條計(jì)算耗時(shí)1ms ~ 2msJieba分詞為主要耗時(shí)向量化單條耗時(shí)30ms ~ 80ms取決于文本長(zhǎng)度向量索引檢索耗時(shí)1ms ~ 10ms使用annoy索引查詢?nèi)鞒唐骄鶈螚l耗時(shí)2ms ~ 5ms95%以上不需走向量層這套配置下單機(jī)日處理量能達(dá)到幾十萬條文本的增量全流程去重對(duì)大多數(shù)爬蟲項(xiàng)目完全夠用。5. 踩坑實(shí)錄去重引擎最容易翻車的五個(gè)細(xì)節(jié)5.1 編碼與亂碼去重前必須做的字符清理中文爬蟲最容易遇到的就是編碼問題。有的頁(yè)面是GBK有的是UTF-8還有的頁(yè)面頭聲明和實(shí)際編碼不一致。如果不清洗干凈就做哈希同樣的內(nèi)容因?yàn)榫幋a不同會(huì)得到完全不同的摘要去重直接失效。我踩過的坑是某個(gè)站點(diǎn)返回的頁(yè)面里帶有大量\u3000全角空格和\xa0不間斷空格兩條一模一樣的正文一條有這些特殊字符一條沒有哈希完全對(duì)不上。后來的處理策略是在規(guī)范化函數(shù)里統(tǒng)一用NFKC標(biāo)準(zhǔn)化先轉(zhuǎn)換字符寬度和兼容字符再顯式替換掉特殊空格。text text.replace(\u3000, ).replace(\xa0, )5.2 閾值誤殺的代價(jià)比想象中大語(yǔ)義去重最怕的不是漏過重復(fù)而是把正常文章誤判為重復(fù)丟棄。曾經(jīng)有個(gè)項(xiàng)目為了“更高效地清洗數(shù)據(jù)”把語(yǔ)義相似度閾值從0.90調(diào)高到0.95結(jié)果一周之后發(fā)現(xiàn)不少獨(dú)立成文但內(nèi)容主題接近的稿件全部被吞了。尤其是同一行業(yè)的新聞比如“某某公司發(fā)布財(cái)報(bào)”這類事件性報(bào)道不同媒體寫的角度不同但核心關(guān)鍵詞高度重合很容易被誤判。我的經(jīng)驗(yàn)是閾值寧低勿高漏判可以靠人工或后續(xù)規(guī)則補(bǔ)救誤殺直接造成數(shù)據(jù)損失而且很難追溯。對(duì)拿不準(zhǔn)的相似候選可以丟到一個(gè)人工審核隊(duì)列里而不是直接丟棄。5.3 模板噪音會(huì)讓語(yǔ)義判斷失真爬蟲抽出來的正文里經(jīng)常夾帶“網(wǎng)友評(píng)論”“熱門評(píng)論”“相關(guān)推薦”這些欄目名甚至還有網(wǎng)站的統(tǒng)計(jì)代碼殘留。這些模板噪音會(huì)影響SimHash和向量計(jì)算的準(zhǔn)確性。比如兩篇完全不同的新聞?wù)睦锒紟е粋€(gè)版權(quán)聲明部分相似度會(huì)被這些噪音拉高容易造成誤判。解決思路是建立“噪音詞庫(kù)”和“模板區(qū)塊識(shí)別”在規(guī)范化階段把頻繁出現(xiàn)的頁(yè)腳、導(dǎo)航、廣告詞直接剝離。更極端的做法是用正文抽取算法比如針對(duì)中文的通用抽取規(guī)則先提取主內(nèi)容區(qū)域再做去重判斷。5.4 向量模型更新導(dǎo)致指紋不統(tǒng)一當(dāng)我決定升級(jí)embedding模型時(shí)遇到過一個(gè)嚴(yán)重的兼容性問題新舊模型產(chǎn)出的向量維度都不同舊索引完全沒法用。如果只是把舊向量全部刪除重新計(jì)算數(shù)據(jù)量太大如果不刪除新舊向量混在一起相似度比較結(jié)果就會(huì)失真。我的做法是切換模型時(shí)先在測(cè)試環(huán)境用新舊模型分別向量化一批樣本確認(rèn)兩者相似度分布一致再準(zhǔn)備一個(gè)回滾期?;貪L期內(nèi)保留舊模型索引新模型索引并行構(gòu)建構(gòu)建完成后由開關(guān)控制切換。這個(gè)流程雖然笨但能保證線上服務(wù)不中斷。5.5 糾錯(cuò)機(jī)制允許“誤殺”但要為“被誤殺”留后路一個(gè)去重引擎如果只做丟棄沒有糾錯(cuò)機(jī)制早晚會(huì)出問題。我的項(xiàng)目里最終加了一個(gè)兜底方案所有被語(yǔ)義層判定為重復(fù)的文本不會(huì)直接丟棄而是存入一個(gè)duplicate_candidates表保留原始文本、被判定重復(fù)的兩條文本ID、相似度、判定時(shí)間。人工審核時(shí)只要打開這個(gè)表就能看到為什么被判重然后把誤判的條目恢復(fù)并加入白名單。白名單里的內(nèi)容在精確層和語(yǔ)義層都會(huì)被跳過防止同樣的誤判反復(fù)發(fā)生。最后再分享一點(diǎn)個(gè)人體會(huì)去重引擎真正難的不是算法本身而是它跟數(shù)據(jù)質(zhì)量緊密綁定。同樣的閾值、同樣的指紋機(jī)制換一個(gè)數(shù)據(jù)源就可能完全失效。我自己的迭代路徑是先做精確去重跑通整個(gè)流程積累起一批真實(shí)重復(fù)數(shù)據(jù)之后再根據(jù)失敗case決定要不要上SimHash、要不要上向量模型。一上來就上大模型只會(huì)讓系統(tǒng)又慢又貴還未必解決實(shí)際問題。另外去重結(jié)果一定要有可觀測(cè)性。我把精確層命中數(shù)、語(yǔ)義層命中數(shù)、疑似重復(fù)候選數(shù)、誤殺恢復(fù)數(shù)都做成了指標(biāo)每天盯一遍。只要這些數(shù)字出現(xiàn)異常波動(dòng)往往意味著某個(gè)網(wǎng)站改版了、某個(gè)模板變了或者某個(gè)新數(shù)據(jù)源有問題。去重引擎做到最后其實(shí)就是用規(guī)則對(duì)付噪音用向量對(duì)付改寫用人工對(duì)付邊界三者缺一不可。