KES向量數(shù)據(jù)庫(kù)實(shí)戰(zhàn):一條SQL干掉ETL,機(jī)器人不再把停產(chǎn)貨當(dāng)現(xiàn)貨賣(mài))
一、背景一套跑了一年的三件套1.1 先說(shuō)項(xiàng)目我在一家制造企業(yè)做平臺(tái)開(kāi)發(fā)。去年接了個(gè)活兒給內(nèi)部客服和售后搭智能問(wèn)答說(shuō)穿了就是 RAG。把幾十年的產(chǎn)品手冊(cè)、維修案例、工單記錄喂給大模型一線員工提問(wèn)的時(shí)候系統(tǒng)把最相關(guān)的資料片段撈出來(lái)遞給模型模型再組織成人話回答。去年上的線。用的人不少日均幾千次提問(wèn)吧。別看我現(xiàn)在說(shuō)得輕巧當(dāng)年的架構(gòu)可是照著行業(yè)標(biāo)配抄的三件套。MySQL 存業(yè)務(wù)元數(shù)據(jù)和文檔目錄一個(gè)專(zhuān)用向量數(shù)據(jù)庫(kù)存 embedding中間靠 ETL 任務(wù)搬磚。就這套東西跑了一年。1.2 當(dāng)時(shí)是怎么切的分工大概這樣。文檔正文和切片進(jìn) MySQL運(yùn)營(yíng)同事要審核管理嘛。切片算出來(lái)的 embedding 灌進(jìn)向量庫(kù)建 ANN 索引管相似度檢索。至于權(quán)限、產(chǎn)品線、有效性這些業(yè)務(wù)屬性兩邊都存了一份向量庫(kù)里放元數(shù)據(jù)字段用來(lái)過(guò)濾MySQL 里那份當(dāng)權(quán)威版本。算法組的小林當(dāng)時(shí)就嘀咕過(guò)說(shuō)兩邊都存早晚要出事。我還挺不耐煩回他說(shuō) ETL 十分鐘跑一次呢能出啥事。嗯。這個(gè) Flag 立得相當(dāng)標(biāo)準(zhǔn)。二、出事了機(jī)器人把停產(chǎn)貨當(dāng)現(xiàn)貨賣(mài)2.1 周二下午售后群炸了。機(jī)器人信誓旦旦地告訴客戶某款停產(chǎn)大半年的控制器目前有貨可以下單。客服沒(méi)多想照著回了。客戶還真去下了單。我到現(xiàn)在都記得排查的過(guò)程。順著問(wèn)題向量去檢索命中的確實(shí)是那款產(chǎn)品的資料相似度還挺高檢索這步?jīng)]毛病。回頭查元數(shù)據(jù)MySQL 里這條產(chǎn)品三個(gè)月前就標(biāo)記停售了。卡在哪兒ETL。某次向量庫(kù)升級(jí)之后同步任務(wù)的鑒權(quán)配置失效了。注意啊任務(wù)沒(méi)報(bào)錯(cuò)它就是安靜地不干活了。停售標(biāo)記從頭到尾沒(méi)走到向量庫(kù)的過(guò)濾字段里。更頭皮發(fā)麻的是這種安靜失敗不是頭一回。上一次是權(quán)限字段某部門(mén)的文檔下線了向量庫(kù)里的副本還活得好好的測(cè)試同學(xué)閑逛才發(fā)現(xiàn)的。兩次一個(gè)根兒。同一份業(yè)務(wù)事實(shí)存兩個(gè)系統(tǒng)靠異步任務(wù)保持一致那延遲和漂移就不是會(huì)不會(huì)發(fā)生的問(wèn)題了只是什么時(shí)候被人撞見(jiàn)的問(wèn)題。2.2 復(fù)盤(pán)會(huì)上算的賬復(fù)盤(pán)會(huì)開(kāi)完我沒(méi)走一個(gè)人把這一年的賬攤開(kāi)來(lái)算了算。同步鏈路四道工序解析、切片、向量化、搬運(yùn)哪道都可能卡住監(jiān)控只蓋得住兩道。混合查詢也別扭用戶的真實(shí)問(wèn)題從來(lái)不是純相似度是某產(chǎn)品線、我有權(quán)限、還沒(méi)過(guò)期的資料里搜這么一串。向量庫(kù)的元數(shù)據(jù)過(guò)濾又弱只能應(yīng)用層先查 MySQL 拿 id再攥著 id 列表去向量庫(kù)搜。權(quán)限范圍一大列表好幾千個(gè)性能當(dāng)場(chǎng)趴窩。運(yùn)維是雙份的苦。兩套庫(kù)兩套備份兩套告警。老徐半夜被告警吵醒原話是我還得先想半分鐘這次是誰(shuí)家的。那天下班路上我一直在想小林那句話。三、換思路讓向量長(zhǎng)回?cái)?shù)據(jù)庫(kù)里3.1 一個(gè)樸素得要命的問(wèn)題后來(lái)某次內(nèi)部討論小林又開(kāi)口了。向量不就是表里的一列嗎憑什么不能跟別的字段住一張表、進(jìn)同一個(gè)事務(wù)會(huì)議室安靜了幾秒。這問(wèn)題聽(tīng)著簡(jiǎn)單把我們整個(gè)架構(gòu)的前提給問(wèn)松了。順著這個(gè)方向查資料最后落在金倉(cāng)數(shù)據(jù)庫(kù) KES 的向量能力上。思路跟小林那個(gè)問(wèn)題一模一樣向量作為一種數(shù)據(jù)類(lèi)型直接建在關(guān)系表里跟數(shù)值、文本、JSON、GIS、時(shí)序一塊兒活在同一個(gè)庫(kù)里。沒(méi)有搬運(yùn)這回事一條 SQL 里向量檢索和普通條件一起下。坦白講我起先犯嘀咕。專(zhuān)用向量庫(kù)那邊迭代得跟飛一樣數(shù)據(jù)庫(kù)內(nèi)置的向量能力會(huì)不會(huì)是個(gè)玩具親手測(cè)了兩件事才踏實(shí)。一是人家自研實(shí)現(xiàn)了 HNSW 和 IVFFlat 兩種近似索引還帶 SIMD 指令級(jí)的加速不是掃個(gè)表糊弄事。二是向量數(shù)據(jù)直接吃數(shù)據(jù)庫(kù)的 ACID 事務(wù)這個(gè)市面上不少 NoSQL 形態(tài)的向量庫(kù)給不了而對(duì)我們這種元數(shù)據(jù)改了向量必須跟著變的場(chǎng)景恰恰是要命的那條。3.2 新架構(gòu)長(zhǎng)什么樣改造完我盯著看了半天有點(diǎn)不習(xí)慣就這么簡(jiǎn)單原來(lái)元數(shù)據(jù)和向量分兩個(gè)庫(kù)、ETL 搬著磚、滯后還漂移現(xiàn)在一張表向量就是其中一列改完即生效。原來(lái)相似度和過(guò)濾分兩步走、應(yīng)用層拿針線縫合現(xiàn)在一條 SQL 混合檢索全下推。原來(lái)備份監(jiān)控權(quán)限配置什么都得來(lái)兩份現(xiàn)在一份。老徐的原話是晚上能睡整覺(jué)了。四、動(dòng)手改造4.1 建表文檔切片表直接帶上向量列。我們的 embedding 模型輸出 768 維類(lèi)型聲明里就寫(xiě) 768別的不用多想CREATETABLEdoc_chunks(id BIGSERIALPRIMARYKEY,doc_idBIGINTNOTNULL,doc_titleTEXT,chunk_textTEXTNOTNULL,embedding VECTOR(768)NOTNULL,product_lineTEXT,is_activeBOOLEANDEFAULTTRUE,dept_permTEXT[],updated_at TIMESTAMPTZDEFAULTnow());多看一眼 is_active 和 dept_perm 這倆字段。停產(chǎn)標(biāo)記、部門(mén)權(quán)限原來(lái)住在另一個(gè)庫(kù)里現(xiàn)在跟向量睡同一行。產(chǎn)品停售了一條 UPDATE 把 is_active 置掉事務(wù)提交那一瞬間檢索結(jié)果里就再也見(jiàn)不著它了。開(kāi)頭那個(gè)事故擱這個(gè)架構(gòu)下壓根沒(méi)有發(fā)生的土壤。4.2 索引選型加兩個(gè)血淚坑兩種索引我們都實(shí)測(cè)了代碼一起貼給你-- HNSW多層圖結(jié)構(gòu)召回高、查詢快就是吃內(nèi)存、建得慢CREATEINDEXidx_chunk_hnswONdoc_chunksUSINGhnsw(embedding vector_cosine_ops);-- IVFFlat先聚類(lèi)再倒排省內(nèi)存、建得快拿 probes 調(diào)精度CREATEINDEXidx_chunk_ivfONdoc_chunksUSINGivfflat(embedding vector_cosine_ops);最后定的 HNSW。語(yǔ)料百萬(wàn)級(jí)內(nèi)存扛得住而客服場(chǎng)景對(duì)召回率敏感漏一條關(guān)鍵維修案例可比慢十毫秒嚴(yán)重多了。你要是向量上億、內(nèi)存又緊那 IVFFlat 值得先試。補(bǔ)一句這兒的索引在數(shù)據(jù)插入更新時(shí)是實(shí)時(shí)維護(hù)的不存在建完索引就不讓改數(shù)據(jù)那種憋屈事。坑踩了倆。第一個(gè)IVFFlat 千萬(wàn)別在空表上建。它先對(duì)全量數(shù)據(jù)聚類(lèi)再分桶空表建出來(lái)的分桶全是錯(cuò)的召回慘到?jīng)]法看。我們當(dāng)時(shí)還以為是版本 bug排了一晚上最后發(fā)現(xiàn)是自己用法不對(duì)。先灌數(shù)后建索引順序別反。第二個(gè)更隱蔽。用余弦距離的話向量入庫(kù)前要?dú)w一化不然模長(zhǎng)會(huì)摻進(jìn)距離里搗亂排序結(jié)果莫名其妙地不對(duì)。我們有一陣子檢索質(zhì)量忽好忽壞查了好幾天才定位到這兒說(shuō)起來(lái)都是淚。4.3 檢索就一條 SQL改造后最爽的部分。用戶提問(wèn)的完整檢索就這一條-- q_vec 是應(yīng)用側(cè)把用戶問(wèn)題向量化后傳進(jìn)來(lái)的參數(shù)SELECTdoc_title,chunk_text,embeddingq_vecASdistanceFROMdoc_chunksWHEREis_activeTRUEANDproduct_line工業(yè)控制器ANDdept_perm ARRAY[after_sales]ORDERBYembeddingq_vecLIMIT5;看看 WHERE 里混了些什么。有效性產(chǎn)品線數(shù)組權(quán)限判斷再搭上 算的余弦距離排序。原來(lái)那個(gè)先查 id 列表再傳給向量庫(kù)的兩段式整個(gè)被壓進(jìn)數(shù)據(jù)庫(kù)一次執(zhí)行。權(quán)限列表幾千條也不虛了過(guò)濾條件下推掃描范圍先砍一大截。我觀察下來(lái)這是融合架構(gòu)最被低估的地方。大家盯著向量檢索性能比來(lái)比去很少有人算省掉的那條同步鏈路值多少錢(qián)。4.4 意外收獲順手的驚喜值得一小節(jié)。向量類(lèi)型支持加減、數(shù)乘、拼接還帶 AVG、SUM 這類(lèi)聚合。我拿它算過(guò)全部語(yǔ)料向量的均值專(zhuān)門(mén)撈離群的切片-- 找跟語(yǔ)料中心離得最遠(yuǎn)的切片多半是切壞了的或者內(nèi)容異常的WITHcenterAS(SELECTAVG(embedding)AScFROMdoc_chunks)SELECTid,doc_title,embeddingcASdistanceFROMdoc_chunks,centerORDERBYdistanceDESCLIMIT20;跑出來(lái)一瞧果然一堆切稀碎的表格和亂碼 PDF。以前這種活兒得導(dǎo)出去寫(xiě) Python 腳本現(xiàn)在 SQL 一條。五、跑了三個(gè)月的賬說(shuō)幾個(gè)拿得出手的數(shù)。檢索鏈路里徹底沒(méi)有 ETL 這號(hào)角色了數(shù)據(jù)新鮮度從最多滯后十分鐘變成事務(wù)級(jí)。混合檢索的 P95 從 180ms 掉到 40ms 上下大頭是省了兩段式那個(gè)應(yīng)用層往返。運(yùn)維對(duì)象兩套變一套老徐的告警群肉眼可見(jiàn)地清凈了。至于停產(chǎn)產(chǎn)品當(dāng)現(xiàn)貨賣(mài)這類(lèi)事故架構(gòu)上沒(méi)有它發(fā)生的土壤了。代價(jià)也得老實(shí)交代。單一庫(kù)資源共享高峰期向量檢索和普通業(yè)務(wù)查詢會(huì)搶 CPU。我們靠資源隔離配置壓了壓不算完美但跟伺候同步鏈路比省心太多。六、寫(xiě)在最后這回改造給我最大的觸動(dòng)是AI 應(yīng)用的架構(gòu)難題好多時(shí)候不在模型身上在數(shù)據(jù)怎么組織。向量數(shù)據(jù)庫(kù)這個(gè)詞聽(tīng)著像要開(kāi)新世界扒開(kāi)看向量就是一種數(shù)據(jù)形態(tài)嘛它在關(guān)系庫(kù)里完全能活得好好的還順手把事務(wù)、權(quán)限、混合查詢這些老手藝全帶過(guò)來(lái)了。一套數(shù)據(jù)庫(kù)解決所有問(wèn)題這話說(shuō)得太滿我不學(xué)。但對(duì)中等規(guī)模的 AI 應(yīng)用我的建議就一句先別急著上第三套存儲(chǔ)看看手頭的庫(kù)能不能把向量裝下多半能省出一條同步鏈路外加好幾個(gè)不眠之夜。下一步打算把工單的時(shí)序數(shù)據(jù)也并進(jìn)來(lái)讓檢索結(jié)果帶上這故障最近是不是高發(fā)的判斷。庫(kù)就在那兒一張表的事兒。老徐說(shuō)了這回他等著看。