国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

向量數(shù)據(jù)庫入坑:傳統(tǒng)文本檢索方式的降維打擊,使用 Faiss 實現(xiàn)向量語義檢索

這篇具有很好參考價值的文章主要介紹了向量數(shù)據(jù)庫入坑:傳統(tǒng)文本檢索方式的降維打擊,使用 Faiss 實現(xiàn)向量語義檢索。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

在上一篇文章《聊聊來自元宇宙大廠 Meta 的相似度檢索技術(shù) Faiss》中,我們有聊到如何快速入門向量檢索技術(shù),借助 Meta AI(Facebook Research)出品的 faiss 實現(xiàn)“最基礎(chǔ)的文本內(nèi)容相似度檢索工具”,初步接觸到了“語義檢索”這種對于傳統(tǒng)文本檢索方式具備“降維打擊”的新興技術(shù)手段。

有朋友在聊天中提到,希望能夠聊點更具體的,比如基于向量技術(shù)實現(xiàn)的語義檢索到底比傳統(tǒng)文本檢索強多少,以及是否有局限性,能不能和市場上大家熟悉的技術(shù)產(chǎn)品進行一個簡單對比。

那么,本篇文章就試著從這個角度來聊聊。

寫在前面

相信有從本文才開始“入坑”、對標題中的 faiss 、向量檢索并不熟悉的朋友,簡單來說,faiss 是一個非常棒的開源項目,也是目前最流行的、效率比較高的文本相似度檢索方案之一。

雖然它和相似度檢索這門技術(shù)頗受歡迎,在出現(xiàn)在了各種我們所熟知的“大廠”應(yīng)用的功能中,但畢竟屬于小眾場景,有著不低的掌握門檻和復(fù)雜性。在之前的內(nèi)容中,我們已經(jīng)介紹過了 faiss ,所以本文就不再展開贅述了。

所以,如果你實在懶得了解它,但是希望能夠和寫簡單的 Web 項目一樣,寫幾行 CRUD 就能夠完成高效的向量檢索功能,建議可以試試啟動一個 Milvus 實例。或者更“懶一些”的話,可以試著使用 Milvus 的 Cloud 服務(wù),來完成高性能的向量檢索。

而傳統(tǒng)文本檢索方面,我將使用簡單的 Golang 來實現(xiàn)一些例子,以及使用我們熟悉的 MySQL來進行功能實現(xiàn)和對比,包含:“LIKE操作符,模式匹配”和“全文檢索”兩種方式。(Elasticsearch 和 Meilisearch,我們在后續(xù)的文章中再展開聊。)

本文中的相關(guān)代碼,均已上傳至 soulteary/text-retrieval-example,有需要的同學可以自行下載。在展開實戰(zhàn)之前,我們先來了解下什么是“文本檢索”。

無處不在的“文本檢索”

“文本檢索”這個詞大家或許會感到陌生,但它或許是我們每天和數(shù)字世界打交道最頻繁的交互模式之一:

  • 從在文檔中使用 “CTRL+F” 快捷鍵查找某個關(guān)鍵詞(在文本中使用文本字詞、短句進行檢索);
  • 在微信、微博、知乎等應(yīng)用里搜索聯(lián)系人、新鮮事兒,在各種搜索引擎中輸入我們要查詢到內(nèi)容(使用文本進行內(nèi)容檢索和匹配);
  • 每天可能會使用的 “AI 音箱”,基本都會使用到的“自動語音識別技術(shù)”也是將語音轉(zhuǎn)換為文本,再對文本進行檢索和匹配;
  • 甚至,我們從出生到告別世界需要在各種信息系統(tǒng)中登記、查詢,也都離不開這個技術(shù)…

是不是看上去都“挺簡單”的?但如果我們在查詢的時候,不能夠完全精準進行“關(guān)鍵詞”匹配,比如在“小智,今天天氣是不是挺好的”這句話里,嘗試直接搜索匹配“今天天兒怎么樣”,會得到什么樣的結(jié)果呢?

普通的軟件將告訴我們“找不到匹配結(jié)果”,有一部分聰明一些的,將幫助我們高亮“今天天”這幾個相同的字符內(nèi)容,但如果我們嘗試搜索“天氣情況”,那么大概率是查不到任何結(jié)果的。

那么,聰明的程序員們是怎么解決這個問題的呢?

文本檢索的發(fā)展史:如何解決搜不到內(nèi)容的問題

既然使用完全匹配得不到結(jié)果,不妨換個思路:使用某種方式,來實現(xiàn)近似結(jié)果的檢索、匹配(相似性檢索)。為了得到“近似的結(jié)果”,我們一般有兩條路可以走。

在還沒有大規(guī)模使用“AI技術(shù)”之前(算法和模型應(yīng)用),為了解決這個問題,聰明的工程師們發(fā)明了“文本相似度計算”。比如,根據(jù)文本的長短(句子、段落、文章)來切分內(nèi)容,接著使用簡單的算法來完成文本相似度的計算。最常見的算法有:編輯距離算法、統(tǒng)計重復(fù)字符出現(xiàn)比例等。

所以,在一些較為“機智”的產(chǎn)品策略里,當我們沒有完全匹配的內(nèi)容時,會呈現(xiàn)給用戶部分匹配的查詢結(jié)果,比如前文中提到的那個例子。但是,**這種的模式解決不了相同語義不同表述方式內(nèi)容的查找和匹配。**比如,我們在上一篇文章中嘗試搜索“哈利波特猛然睡醒”這種基本在原文中沒有的內(nèi)容。

伴隨著人工智能 NLP 領(lǐng)域技術(shù)的迅猛發(fā)展,聰明的工程師們又折騰出了“新的解決方案”,先將文本內(nèi)容進行各種維度的切分,接著將它們轉(zhuǎn)換為向量數(shù)據(jù),然后實現(xiàn)出基于統(tǒng)計特征(TF/TF-IDF/Simhash)或者基于語義的特征模型(word2vec、doc2vec),最后搭建一套推理服務(wù),就能夠解決基于語義的文本匹配啦。其中常見的用于判斷相似度的算法包括:歐氏距離、漢明距離、余弦距離等。

隨后,NLP 領(lǐng)域中的“文本相似度計算方向”百花齊放,機智的學者們和聰明的程序員們,通過各種方式不斷完善“語義文本相似度計算”(Semantic Textual Similarity)技術(shù),實現(xiàn)出了越來越準確、高效的工具,能夠?qū)Ω鞣N內(nèi)容進行文本的分類和聚類、歧義消除等傳統(tǒng)方案解決的非常不好的問題。不僅僅針對前文中提到的使用場景和案例,還有一些我們常常使用的:抄襲檢測(論文、專利、文章)、自動文本摘要(新聞)、機器翻譯(比如微信消息)、機器人問答系統(tǒng)(各種App里的客服)、多模態(tài)檢索(比如智能音箱)…

好了,相信聰明的你一定已經(jīng)看明白了,文本檢索的發(fā)展歷程,以及迫不及待的想要開始實踐啦。

使用 Golang 實踐傳統(tǒng)文本檢索

現(xiàn)代編程語言在基礎(chǔ)的文本操作方面都差不多,考慮到演示方便,這里選擇使用 Golang 來完成 Demo:這里選擇一首我很喜歡的詩作為例子,來實現(xiàn)一個簡單的程序,針對它進行內(nèi)容查找(文本檢索)。

西風吹老洞庭波,一夜湘君白發(fā)多。
醉后不知天在水,滿船清夢壓星河。

使用 Golang 實現(xiàn)基礎(chǔ)的文本檢索功能

我們先來實現(xiàn)一個最簡單的“文本完全匹配/包含”的例子。新建一個文件,取名為 simple.go,然后實現(xiàn)一個最基礎(chǔ)的文本匹配功能 SimpleMatch

package main

import (
	"strings"
)

func SimpleMatch(str1, str2 string) bool {
	if len(str1) >= len(str2) {
		return strings.Contains(str1, str2)
	}
	return strings.Contains(str2, str1)
}

上面的內(nèi)容非常簡單,實現(xiàn)了在比較長的字符串中嘗試搜索匹配短字符串的功能。在完成這個“最簡單算法”之后,我們編寫程序來進行簡單例子的驗證:

package main

import (
	"fmt"
	"testing"
)

func TestSimpleMatch(t *testing.T) {
	var tests = []struct {
		A string
		B string
	}{
		{"西風吹老洞庭波,一夜湘君白發(fā)多。", "洞庭湖"},
		{"醉后不知天在水,滿船清夢壓星河。", "滿船清夢"},
		{"2022", "2023"},
		{"真不錯", "還不錯"},
	}

	for _, str := range tests {
		testname := fmt.Sprintf("測試內(nèi)容是否匹配:“%s” vs “%s”", str.A, str.B)
		t.Run(testname, func(t *testing.T) {
			ret := SimpleMatch(str.A, str.B)
			fmt.Println("內(nèi)容匹配:", ret)
		})
	}
}

這里我們使用 Go 內(nèi)置的單元測試功能,來完成函數(shù)的調(diào)用以及結(jié)果的展示。將上面的內(nèi)容保存為 simple_test.go ,然后執(zhí)行 go test -v,我們能夠看到下面的結(jié)果:

=== RUN   TestSimpleMatch
=== RUN   TestSimpleMatch/測試內(nèi)容是否匹配:“西風吹老洞庭波,一夜湘君白發(fā)多?!盻vs_“洞庭湖”
內(nèi)容匹配: false
=== RUN   TestSimpleMatch/測試內(nèi)容是否匹配:“醉后不知天在水,滿船清夢壓星河?!盻vs_“滿船清夢”
內(nèi)容匹配: true
=== RUN   TestSimpleMatch/測試內(nèi)容是否匹配:“2022”_vs_“2023”
內(nèi)容匹配: false
=== RUN   TestSimpleMatch/測試內(nèi)容是否匹配:“真不錯”_vs_“還不錯”
內(nèi)容匹配: false

可以看到和我們前文中提到的情況一樣,程序只能夠識別“完全相同的部分”。接下來,我們來實現(xiàn)一個常見的相似度計算算法:編輯算法。來解決我們要查找的內(nèi)容和被查找內(nèi)容“不能完全匹配”場景下的內(nèi)容檢索。

基于字符的相似度計算:編輯距離算法(levenshtein)

在“古老的” PHP 中,內(nèi)置了一個名為 levenshtein(萊文斯坦,也被稱作編輯距離)的函數(shù)。我們可以用這個函數(shù)來計算兩個字符串之間的相似度。這里偷個懶,我們直接使用開源項目 syyongx/php2go 中已經(jīng)實現(xiàn)好的內(nèi)容,來幫助我們加速完成這部分基于字符的相似度計算實戰(zhàn)。

“編輯距離(Levenshtein)”算法,是一種比較簡單的求兩個字符串之間相似度的算法。簡單來說,就是通過計算一個字符串需要經(jīng)過多少次對內(nèi)容“增刪改”,才能完全變?yōu)榱硗庖粋€字符串的方式,來實現(xiàn)針對兩個字符串內(nèi)容差異程度的量化。

我們假設(shè)將一個文本轉(zhuǎn)換為另一個文本,過程中的添加字符、刪除字符、替換字符都記做一次操作的話,算法的 Golang 版本實現(xiàn)類似下面這樣:

package main

// Levenshtein levenshtein()
// costIns: Defines the cost of insertion.
// costRep: Defines the cost of replacement.
// costDel: Defines the cost of deletion.
func Levenshtein(str1, str2 string, costIns, costRep, costDel int) int {
	var maxLen = 255
	l1 := len(str1)
	l2 := len(str2)
	if l1 == 0 {
		return l2 * costIns
	}
	if l2 == 0 {
		return l1 * costDel
	}
	if l1 > maxLen || l2 > maxLen {
		return -1
	}

	p1 := make([]int, l2+1)
	p2 := make([]int, l2+1)
	var c0, c1, c2 int
	var i1, i2 int
	for i2 := 0; i2 <= l2; i2++ {
		p1[i2] = i2 * costIns
	}
	for i1 = 0; i1 < l1; i1++ {
		p2[0] = p1[0] + costDel
		for i2 = 0; i2 < l2; i2++ {
			if str1[i1] == str2[i2] {
				c0 = p1[i2]
			} else {
				c0 = p1[i2] + costRep
			}
			c1 = p1[i2+1] + costDel
			if c1 < c0 {
				c0 = c1
			}
			c2 = p2[i2] + costIns
			if c2 < c0 {
				c0 = c2
			}
			p2[i2+1] = c0
		}
		tmp := p1
		p1 = p2
		p2 = tmp
	}
	c0 = p1[l2]

	return c0
}

將上面的內(nèi)容保存為 levenshtein.go,然后再次使用 Go 內(nèi)置的單元測試功能,來完成調(diào)用和展示功能:

package main

import (
	"fmt"
	"testing"
)

func TestLevenshtein(t *testing.T) {
	var tests = []struct {
		A string
		B string
	}{
		{"西風吹老洞庭波,一夜湘君白發(fā)多。", "洞庭湖"},
		{"醉后不知天在水,滿船清夢壓星河。", "滿船清夢"},
		{"2022", "2023"},
		{"真不錯", "還不錯"},
	}

	for _, str := range tests {
		testname := fmt.Sprintf("%s,%s", str.A, str.B)
		t.Run(testname, func(t *testing.T) {
			ret := Levenshtein(str.A, str.B, 1, 1, 1)
			if ret < 0 {
				t.Errorf("計算或例子錯誤 %s, %s", str.A, str.B)
			}
			fmt.Println(ret)
		})
	}
}

將上面的內(nèi)存保存為 levenshtein_test.go ,接著再次執(zhí)行 go test -v,我們將能夠得到下面的結(jié)果:

=== RUN   TestLevenshtein
=== RUN   TestLevenshtein/西風吹老洞庭波,一夜湘君白發(fā)多。,洞庭湖
40
=== RUN   TestLevenshtein/醉后不知天在水,滿船清夢壓星河。,滿船清夢
36
=== RUN   TestLevenshtein/2022,2023
1
=== RUN   TestLevenshtein/真不錯,還不錯
3

結(jié)果中的數(shù)字告訴我們兩個文本字符串想變成一樣的,需要進行多少次操作,數(shù)字越大,則說明文本整體的相似度越低。所以,在實際業(yè)務(wù)使用中,我們只需要將得到的結(jié)果進行排序,選擇數(shù)字最小的結(jié)果進行返回即可。

這個數(shù)字一般被稱作 Levenshtein distance (L氏距離)數(shù)值,類似上面的同類算法還有:LCS(最長公共子序列)、Jaro、漢明距離。

基于字符的相似度計算:字符重復(fù)出現(xiàn)次數(shù)

除了上面基于字符串“距離”進行相似度計算的方式之外,我們還可以基于字符重復(fù)出現(xiàn)次數(shù),來對兩個字符串進行相似度計算。為了偷懶,還是使用開源項目中已經(jīng)實現(xiàn)好的函數(shù) similar_text 來作為演示對象:

package main

// SimilarText similar_text()
func SimilarText(first, second string, percent *float64) int {
	var similarText func(string, string, int, int) int
	similarText = func(str1, str2 string, len1, len2 int) int {
		var sum, max int
		pos1, pos2 := 0, 0

		// Find the longest segment of the same section in two strings
		for i := 0; i < len1; i++ {
			for j := 0; j < len2; j++ {
				for l := 0; (i+l < len1) && (j+l < len2) && (str1[i+l] == str2[j+l]); l++ {
					if l+1 > max {
						max = l + 1
						pos1 = i
						pos2 = j
					}
				}
			}
		}

		if sum = max; sum > 0 {
			if pos1 > 0 && pos2 > 0 {
				sum += similarText(str1, str2, pos1, pos2)
			}
			if (pos1+max < len1) && (pos2+max < len2) {
				s1 := []byte(str1)
				s2 := []byte(str2)
				sum += similarText(string(s1[pos1+max:]), string(s2[pos2+max:]), len1-pos1-max, len2-pos2-max)
			}
		}

		return sum
	}

	l1, l2 := len(first), len(second)
	if l1+l2 == 0 {
		return 0
	}
	sim := similarText(first, second, l1, l2)
	if percent != nil {
		*percent = float64(sim*200) / float64(l1+l2)
	}
	return sim
}

我們將上面的內(nèi)容保存為 similar.go,然后我們來編寫調(diào)用程序 similar_test.go

package main

import (
	"fmt"
	"testing"
)

func TestSimilarText(t *testing.T) {
	var tests = []struct {
		A string
		B string
	}{
		{"西風吹老洞庭波,一夜湘君白發(fā)多。", "洞庭湖"},
		{"醉后不知天在水,滿船清夢壓星河。", "滿船清夢"},
		{"2022", "2023"},
		{"真不錯", "還不錯"},
	}

	for _, str := range tests {
		testname := fmt.Sprintf("%s,%s", str.A, str.B)
		t.Run(testname, func(t *testing.T) {
			percent := float64(0)
			ret := SimilarText(str.A, str.B, &percent)
			if ret < 0 {
				t.Errorf("計算或例子錯誤 %s, %s", str.A, str.B)
			}
			fmt.Println(ret, fmt.Sprintf("%.2f", percent)+"%")
		})
	}
}

將文件保存好之后,我們執(zhí)行 go test -v,將得到下面的結(jié)果:

=== RUN   TestSimilarText
=== RUN   TestSimilarText/西風吹老洞庭波,一夜湘君白發(fā)多。,洞庭湖
8 28.07%
=== RUN   TestSimilarText/醉后不知天在水,滿船清夢壓星河。,滿船清夢
12 40.00%
=== RUN   TestSimilarText/2022,2023
3 75.00%
=== RUN   TestSimilarText/真不錯,還不錯
6 66.67%

結(jié)果中的第一個數(shù)字代表了兩個字符串中最長的連續(xù)重復(fù)內(nèi)容的長度(LCS),第二個數(shù)字則表示了兩個字符串的相似程度。相比上一小節(jié)中的簡單的“編輯距離”計算的可用度高了不少,但依舊解決不了上文中的“語義檢索”的問題

并且,在實際業(yè)務(wù)中,我們需要進行需求可能是“某個文本在一大堆數(shù)據(jù)中的查找”、“許多文本在一大堆數(shù)據(jù)中的查找”。這個時候,上面樸素的算法顯然無法滿足我們的需求。

至于關(guān)于如何實現(xiàn)語義檢索,我們等會聊。先來看看如何使用傳統(tǒng)檢索技術(shù)來解決“一對多”、“多對多”這種場景下的內(nèi)容查找問題吧。

全文檢索

提到全文檢索,我們會很自然的想到老牌工具 Elasticsearch 和 Apache Solr 這類基于 Lucene 的全文檢索方案。這類使用倒排索引幫助用戶實現(xiàn)數(shù)據(jù)定位查找的工具最明顯的特點就是快,以及可能不那么準(一會說為啥)。

倒排索引(Inverted index)為何而生?

簡單想象一下,如果我們想要用上文中的程序完成對互聯(lián)網(wǎng)網(wǎng)頁中的文本內(nèi)容的處理,對其中包含的某個詞或者短語進行文本相似度計算,將會有一個非??膳碌慕Y(jié)果:我們需要等待程序?qū)γ恳黄獌?nèi)容進行計算,當所有內(nèi)容都計算完畢之后,我們才能得到結(jié)果。如果我們要查找多個內(nèi)容,并且我們要查找的互聯(lián)網(wǎng)內(nèi)容還在持續(xù)、不斷的快速增長,這個計算過程將非常難完成,我們也將難以像現(xiàn)在一樣,快速的從互聯(lián)網(wǎng)的搜索引擎中得到想要的結(jié)果。

所以聰明的程序員們,就發(fā)明了“倒排索引”。通過一些程序計算,先對可以被查詢的文檔、文本內(nèi)容進行內(nèi)容切分(分詞),然后針對這些切分好的內(nèi)容進行編號,做成一張大的表格,當我們需要查詢某個內(nèi)容的時候,如果是詞語就直接在表里查詢它的編號,然后返回和這個編號綁定的內(nèi)容的數(shù)據(jù),類似我們常用到的 “KV” 查詢數(shù)值,犧牲一定的存儲空間,降低計算復(fù)雜度,換取高速的數(shù)據(jù)吞吐能力,讓我們能快速的找到想要的結(jié)果。如果我們要查詢的內(nèi)容是句子或者更長的片段的話,則會先進行類似被查詢內(nèi)容的切分操作,分別用不同的詞進行并行查找,找到各個詞的編號,以及編號背后的數(shù)據(jù),然后返回給我們。

雖然看起來一切美好,但是如果我們提供的詞不能夠被很好的進行分詞呢?比如這兩本書:《無線電法國別研究》、《物理學家用微分幾何》。**當遇到這類分詞存在歧義的內(nèi)容時,我們想得到預(yù)期內(nèi)的結(jié)果,還是有一些挑戰(zhàn)的。**并且,前文提到了,這個方案,也解決不了我們想知道“過幾天天天天氣不好”這類需要使用語義檢索來解決的問題。

但是,為了大家能夠更深入的理解概念和原理,我們還是來實踐下這種傳統(tǒng)的檢索技術(shù)吧。

基于 MySQL 全文索引來進行文本檢索

我們可以選擇配置和使用相比較 “ES” 和 Solar 更為簡單的 MySQL 的 “全文索引” 來完成對傳統(tǒng)文本檢索的基礎(chǔ)認識。

為了更好的驗證全文索引的效果,我們可以加大被查詢的文本數(shù)據(jù)量。這里選擇使用開源項目 modood/Administrative-divisions-of-China 中的“國內(nèi)所有的街道名稱”的數(shù)據(jù),來實現(xiàn)一個基于 MySQL 全文檢索的“地址模糊查詢功能”,預(yù)期效果類似我們在各種購物軟件的收貨地址管理功能中,輸入部分地名關(guān)鍵詞的時候,App 自動補全出最有可能的結(jié)果。

為了操作盡可能簡單、以及盡可能節(jié)約我們的折騰時間,我這里選擇使用 MySQL 官方提供的 Docker 鏡像,通過容器的方式來完成 MySQL 的“部署”和參數(shù)配置。為了讓 MySQL 能夠?qū)崿F(xiàn)比較好的短文本場景的檢索效果,我寫了一個 “docker-compose.yml”:

version: '3'

services:

  mysql:
    image: mysql:5.7.39
    container_name: mysql-instance
    command:
      - "--default-authentication-plugin=mysql_native_password"
      - "--character-set-server=utf8mb4"
      - "--collation-server=utf8mb4_unicode_ci"
      - "--init-connect='SET NAMES utf8mb4;'"
      - "--innodb_buffer_pool_size=1M"
      - "--query_cache_type=0"
      - "--query-cache-size=0"
      - "--innodb-flush-log-at-trx-commit=0"
      - "--innodb_buffer_pool_instances=1"
      - "--ft_min_word_len=1"
      - "--ft_stopword_file=''"
      - "--innodb-ft-min-token-size=1"
      - "--innodb-ft-enable-stopword=off"
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=soulteary
      - LANG=C.UTF-8
    volumes:
      - ./data:/var/lib/mysql:rw
      - ./files:/var/lib/mysql-files:rw

為了避免 MySQL 緩存對我們的簡單測試造成影響,我這里通過配置啟動參數(shù)將相關(guān)功能進行了禁用,確保我們在后續(xù)查詢的時候,始終消耗真實的計算時間,而非緩存后的“虛假數(shù)字”。

將上面的內(nèi)容保存為 docker-compose.yml,然后執(zhí)行我們熟悉的 docker-compose up,在 MySQL 下載完畢之后,將按照我們的要求自動的進行初始化,當我們看到類似下面的日志的時候,數(shù)據(jù)庫就準備好啦:

mysql_1  | 2022-09-08T16:06:25.692847Z 0 [Note] InnoDB: Buffer pool(s) load completed at 220908 16:06:25
mysql_1  | 2022-09-08T16:06:25.827887Z 0 [Note] Event Scheduler: Loaded 0 events
mysql_1  | 2022-09-08T16:06:25.828324Z 0 [Note] mysqld: ready for connections.
mysql_1  | Version: '5.7.39'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)

在完成了對 MySQL 的初始化之后,我們可以使用簡單的命令,來確認數(shù)據(jù)庫的參數(shù)狀況,是否如我們在配置中設(shè)置的一樣,先執(zhí)行下面的命令,進入 MySQL CLI 模式:

docker exec -it mysql-instance mysql -uroot -psoulteary

接著,我們輸入下面的兩條 SQL,來檢查配置是否如我們所期望的那樣:

show variables like 'innodb_ft%';
show variables like 'ft%';

可以看到 innodb_ft_min_token_sizeft_min_word_len 都被設(shè)置成了 1,允許針對最少1個字長度的字符進行分詞;innodb_ft_enable_stopwordft_stopword_file 分別設(shè)置成 OFF 和“空”,對“內(nèi)置停用詞”功能進行了禁用,避免效果不佳。

mysql> show variables like 'innodb_ft%';
+---------------------------------+------------+
| Variable_name                   | Value      |
+---------------------------------+------------+
| innodb_ft_aux_table             |            |
| innodb_ft_cache_size            | 8000000    |
| innodb_ft_enable_diag_print     | OFF        |
| innodb_ft_enable_stopword       | OFF        |
| innodb_ft_max_token_size        | 84         |
| innodb_ft_min_token_size        | 1          |
| innodb_ft_num_word_optimize     | 2000       |
| innodb_ft_result_cache_limit    | 2000000000 |
| innodb_ft_server_stopword_table |            |
| innodb_ft_sort_pll_degree       | 2          |
| innodb_ft_total_cache_size      | 640000000  |
| innodb_ft_user_stopword_table   |            |
+---------------------------------+------------+
12 rows in set (0.00 sec)

mysql> show variables like 'ft%';
+--------------------------+----------------+
| Variable_name            | Value          |
+--------------------------+----------------+
| ft_boolean_syntax        | + -><()~*:""&| |
| ft_max_word_len          | 84             |
| ft_min_word_len          | 1              |
| ft_query_expansion_limit | 20             |
| ft_stopword_file         | ''             |
+--------------------------+----------------+
5 rows in set (0.00 sec)

在配置確認無誤之后,我們就可以準備將數(shù)據(jù)灌入數(shù)據(jù)庫中了。我們先下載項目中的數(shù)據(jù)文件 “villages.csv”,將文件復(fù)制到 MySQL 容器啟動之后,容器在當前目錄中自動創(chuàng)建的 “files” 目錄中。

通過使用 head files/villages.csv 命令,簡單查看文件內(nèi)容后,我們可以看到這個數(shù)據(jù)包含了“六個數(shù)據(jù)列”:

code,name,streetCode,provinceCode,cityCode,areaCode
110101001001,"多福巷社區(qū)居委會",110101001,11,1101,110101
110101001002,"銀閘社區(qū)居委會",110101001,11,1101,110101
110101001005,"東廠社區(qū)居委會",110101001,11,1101,110101
110101001006,"智德社區(qū)居委會",110101001,11,1101,110101
110101001007,"南池子社區(qū)居委會",110101001,11,1101,110101
110101001009,"燈市口社區(qū)居委會",110101001,11,1101,110101
110101001010,"正義路社區(qū)居委會",110101001,11,1101,110101
110101001013,"臺基廠社區(qū)居委會",110101001,11,1101,110101
110101001014,"韶九社區(qū)居委會",110101001,11,1101,110101

在這個例子中,我們不需要進行復(fù)雜的“級聯(lián)”操作,所以我們可以將除了前兩列的內(nèi)容都剔除掉。數(shù)據(jù)“ETL”的方式很簡單,使用 awk 的經(jīng)典范式即可:

cat files/villages.csv | awk -F, '{OFS=",";print $1,$2}' > files/data.csv

當命令執(zhí)行完畢,我們就能夠在目錄中得到一份簡單到只有“序號,名稱”的數(shù)據(jù)了:

head files/data.csv
code,name
110101001001,"多福巷社區(qū)居委會"
110101001002,"銀閘社區(qū)居委會"
110101001005,"東廠社區(qū)居委會"
110101001006,"智德社區(qū)居委會"
110101001007,"南池子社區(qū)居委會"
110101001009,"燈市口社區(qū)居委會"
110101001010,"正義路社區(qū)居委會"
110101001013,"臺基廠社區(qū)居委會"
110101001014,"韶九社區(qū)居委會"

如果你希望這些數(shù)據(jù)更像是我們平時使用 App 補全出來的地址,也并不希望文件中第一行數(shù)據(jù)包含“列頭”,可以使用腳本進一步完成對內(nèi)容中名詞的刪除:

cat files/data.csv | sed 's/居委會//g' | sed 's/村委會//g' | sed 's/民委員會//g' > files/pure.csv

完整的數(shù)據(jù)行數(shù)大概有 60 多萬行,雖然相比生產(chǎn)環(huán)境還不夠多,但是足以作為我們的演示使用的例子啦。

cat files/pure.csv | wc -l
  618135

在準備好數(shù)據(jù)之后,我們來將這個數(shù)據(jù)導入 MySQL 容器中,先在容器中創(chuàng)建相關(guān)的“庫表結(jié)構(gòu)”,并設(shè)置為添加到表中的文本字段進行索引建立:

CREATE DATABASE `test`;

CREATE TABLE `test`.`items` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(100) NOT NULL,
  PRIMARY KEY (`id`),
  FULLTEXT KEY (`name`) WITH PARSER ngram
) ENGINE=InnoDB;

為了對中文檢索效果更好,我們需要使用 ngram,來完成索引的建立。接著,將我們處理好的數(shù)據(jù)導入新建好的數(shù)據(jù)表中。

LOAD DATA INFILE '/var/lib/mysql-files/pure.csv'
INTO TABLE `test`.`items`
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS;

因為數(shù)據(jù)在“灌入”的過程中,會進行分詞、建立索引、更新已有索引等計算操作,所以會花費比較長的時間,需要稍微等待一陣,當命令執(zhí)行完畢,我們將會看到類似下面的輸出:

Query OK, 618134 rows affected (20.91 sec)
Records: 618134  Deleted: 0  Skipped: 0  Warnings: 0

當數(shù)據(jù)導入之后,我們就能夠使用 MySQL 的全文索引查詢功能,來進行各種內(nèi)容的檢索了,比如我們先來在 60 萬條數(shù)據(jù)中查找所有包含“青龍”的地名:

SELECT * FROM `test`.`items`
WHERE MATCH (name)
AGAINST ('青龍' IN NATURAL LANGUAGE MODE);

執(zhí)行查詢,我們將會得到類似下面的結(jié)果:

mysql> SELECT * FROM `test`.`items`
    -> WHERE MATCH (name)
    -> AGAINST ('青龍' IN NATURAL LANGUAGE MODE);
+--------------+--------------------------------+
| id           | name                           |
+--------------+--------------------------------+
| 110101005010 | 青龍社區(qū)                       |
| 110108023203 | 青龍橋                         |
| 110109106206 | 青龍澗                         |
| 110111112207 | 青龍頭                         |
| 130209452005 | 青龍湖社區(qū)                     |
| 130324103230 | 青龍港                         |
| 130324108223 | 青龍河                         |
| 130534105238 | 青龍莊村                       |
| 130723100204 | 青龍村村                       |
...
| 621225101214 | 青龍                           |
| 640324102208 | 青龍山                         |
| 654325100009 | 青龍湖                         |
+--------------+--------------------------------+
396 rows in set (0.03 sec)

在 60 萬內(nèi)容中尋找接近 400 個結(jié)果的返回,只用了 0.03s,是不是還蠻驚艷的呢?在完成常規(guī)的關(guān)鍵詞查詢之后,我們來試試“相似文本的查詢匹配”,比如查詢一個我國街道中不存在的地方“青龍朱雀白虎玄武”:

SELECT * FROM `test`.`items`
WHERE MATCH (name)
AGAINST ('青龍朱雀白虎玄武' IN NATURAL LANGUAGE MODE);

在 SQL 執(zhí)行完畢之后,我們將得到和上面稍有不同的“答案”:

mysql> SELECT * FROM `test`.`items`
    -> WHERE MATCH (name)
    -> AGAINST ('青龍朱雀白虎玄武' IN NATURAL LANGUAGE MODE);
+--------------+--------------------------------+
| id           | name                           |
+--------------+--------------------------------+
| 370614106234 | 臥龍朱家                       |
| 330212008010 | 朱雀社區(qū)                       |
| 371323111295 | 朱雀村村                       |
| 433122105202 | 朱雀洞村                       |
| 610103007003 | 朱雀南社區(qū)                     |
| 610103007004 | 朱雀北社區(qū)                     |
| 621226111205 | 朱雀村                         |
| 370705002030 | 玄武東街社區(qū)居                 |
| 430224105214 | 玄武村                         |
...
| 654325100009 | 青龍湖                         |
+--------------+--------------------------------+
445 rows in set (0.03 sec)

上面輸出的結(jié)果,就是我們使用 MySQL 的自然語言模式(Natural Language Full-Text Searches),進行文本相似度查詢的結(jié)果。而這個模式背后的原理,類似我們前文中提到的字符串相似度計算。

通過借助數(shù)據(jù)庫這種“工程藝術(shù)結(jié)晶”,我們就可以達成前文中提到的“一對多”、“多對多”這種場景下的內(nèi)容檢索需求了,完成內(nèi)容的批量查找。

除了自然語言模式之外,MySQL 還內(nèi)置了布爾模式(Boolean Full-Text Searches),感興趣的話,你可以自行了解,這里就不繼續(xù)展開啦。

題外話:有一部分同學 “%LIKE%” 的 MySQL 模式匹配

我知道有一部分同學非常熱衷于 “%LIKE%” 的方式來“解決問題”。在內(nèi)容量比較少的時候,或者硬件能力非常強的時候,這個方式都沒有太大的問題,但是在數(shù)據(jù)量非常大,或者業(yè)務(wù)機器計算資源非常緊張的時候,使用這個方式,會讓性能問題加重,而且還有可能引發(fā)其他的問題。

我們參考上一小節(jié)中的查詢內(nèi)容,進行一個相同例子的查找,并觀察性能表現(xiàn):

SELECT * FROM `test`.`items` WHERE `name` LIKE '%青龍%';

當 SQL 執(zhí)行完畢,我們將得到類似上文中的查詢結(jié)果。

mysql> SELECT * FROM `test`.`items` WHERE `name` LIKE '%青龍%';
+--------------+--------------------------------+
| id           | name                           |
+--------------+--------------------------------+
| 110101005010 | 青龍社區(qū)                       |
| 110108023203 | 青龍橋                         |
| 110109106206 | 青龍澗                         |
| 110111112207 | 青龍頭                         |
| 130209452005 | 青龍湖社區(qū)                     |
...
| 640324102208 | 青龍山                         |
| 654325100009 | 青龍湖                         |
+--------------+--------------------------------+
396 rows in set (0.24 sec)

但是相比較之前怎么查都是 “0.0x” 秒而言,使用全表掃描的 “%LIKE%” 的查詢時間增長了一個數(shù)量級!如果我們查詢的數(shù)據(jù)量不是 60 萬,而是生產(chǎn)環(huán)境上億或者更大規(guī)模的數(shù)據(jù),可能產(chǎn)生的全表掃描行為,除了會帶來大量資源消耗、非常慢甚至不一定能夠得到查詢結(jié)果的情況外,還有可能阻塞正常的寫入操作,造成業(yè)務(wù)異常。

MySQL LIKE 模式匹配查詢的另外一個問題是,并不能夠完成類似上文中“全文索引”提供的相似度計算的能力,如果我們使用相同的例子進行查詢的話:

SELECT * FROM `test`.`items` WHERE `name` LIKE '%青龍白虎朱雀玄武%';

得到的結(jié)果一定是和下面一樣的空結(jié)果:

mysql> SELECT * FROM `test`.`items` WHERE `name` LIKE '%青龍白虎朱雀玄武%';
Empty set (0.20 sec)

所以,綜上所述,如果不是磁盤特別緊張,對于有大量數(shù)據(jù)讀取操作的場景,我們還是盡量合理的建立索引,減少對 “%LIKE%” 的依賴;以及,在需要查詢相似度的時候,選擇更合理的方案。

講到這里,我相信此刻你應(yīng)該比較清楚“傳統(tǒng)文本檢索”技術(shù)是基于哪些套路來完成“內(nèi)容匹配”、“內(nèi)容檢索”、“內(nèi)容相似度計算”,以及如何使用 MySQL 來完成批量內(nèi)容的“文本檢索”,尤其是“相似性檢索”啦。

使用 Faiss 來進行語義檢索

接下來,我們來聊聊對傳統(tǒng)技術(shù)具備降維打擊的“向量語義檢索”技術(shù)。依舊是先來準備 faiss 的運行環(huán)境,完成 faiss 和相關(guān)軟件的安裝。

為了方便你的使用,我寫了一個 “ALL IN ONE” 的 Docker 鏡像構(gòu)建文件。

在 Docker 中完成 Faiss 的安裝

在下面的 Dockerfile 中,我們主要做了三件事:安裝 miniconda、安裝 faiss、安裝所需要的軟件包以及完成模型的下載。如果你想了解為什么這樣做,可以翻閱之前的文章,包含了分步驟的解釋,考慮到篇幅,這里就不再展開了。

FROM python:3.8-bullseye
RUN sed -i -E "s/\w+.debian.org/mirrors.tuna.tsinghua.edu.cn/g" /etc/apt/sources.list

# 安裝 miniconda
ARG CONDA_MIRROR="https://mirrors.tuna.tsinghua.edu.cn/anaconda"
ENV CONDA_MIRROR=${CONDA_MIRROR}
ENV CONDA_SRC="https://repo.anaconda.com/miniconda"
RUN CONDA_SRC="${CONDA_MIRROR}/miniconda"; \
    curl -fsSL -v -o ~/miniconda.sh -O  "$CONDA_SRC/Miniconda3-latest-Linux-x86_64.sh" && \
    chmod +x ~/miniconda.sh && \
    ~/miniconda.sh -b -p ~/miniconda && \
    rm ~/miniconda.sh && \
    echo "channels:" > $HOME/.condarc && \
    echo "  - ${CONDA_MIRROR}/pkgs/free/" >> $HOME/.condarc && \
    echo "  - ${CONDA_MIRROR}/pkgs/main/" >> $HOME/.condarc && \
    echo "  - ${CONDA_MIRROR}/cloud/pytorch/" >> $HOME/.condarc && \
    echo "  - defaults" >> $HOME/.condarc && \
    echo "show_channel_urls: true" >> $HOME/.condarc;
SHELL ["/bin/bash", "-c"]
ENV PATH="~/miniconda/bin:${PATH}"
ARG PATH="~/miniconda/bin:${PATH}"

# 安裝 faiss
ARG PYTHON_VERSION=3.8
RUN conda install -y pytorch python=${PYTHON_VERSION} faiss-cpu pandas && \
    conda clean -ya && \
    conda init bash
# 安裝相關(guān)軟件包
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple && \
    pip install sentence_transformers

WORKDIR /workspace

為了你能夠更快的完成構(gòu)建,我將除了模型之外的相關(guān)軟件下載都使用“清華源”進行了替換。我們將上面的內(nèi)容保存為 Dockerfile,然后執(zhí)行 docker build -t faiss .,等待鏡像的構(gòu)建完畢。

雖然在例子中我們使用的是 cpu 版本的 faiss,但因為同樣依賴大塊頭 pytorch 等,所以鏡像體積會比較大(接近6G),空間緊張的同學需要注意。

當模型構(gòu)建完畢之后,我們就可以來體驗和使用基于“向量相似度檢索”的語義檢索啦。

數(shù)據(jù)準備:人民日報新聞數(shù)據(jù)集

通過之前的實踐,我們已經(jīng)清楚了如何對內(nèi)容進行完全和部分的匹配,為了更直觀的了解“語義檢索”,我們換一個數(shù)據(jù),讓難度提升些,也為了最后的對比效果更明顯一些。

我這里選擇的是來自 Kaggle 的“People’s Daily News”數(shù)據(jù)集,包含了2021 年至今的人民日報報道過的四萬三千多篇內(nèi)容,接近 140 萬長短句內(nèi)容,遠超我們之前在驗證進行批量文本檢索時,在傳統(tǒng)數(shù)據(jù)庫以及全文索引場景時的數(shù)據(jù)量。

ls data/RenMin_Daily/
20210101-01-01.txt 20210304-01-05.txt 20210430-02-06.txt 20210708-17-02.txt 20210907-13-05.txt 20211114-08-03.txt 20220117-04-04.txt 20220323-17-01.txt 20220521-02-03.txt 20220714-03-02.txt
20210101-01-02.txt 20210304-01-06.txt 20210430-02-07.txt 20210708-17-03.txt 20210907-13-06.txt 20211115-01-01.txt 20220117-05-01.txt 20220323-18-01.txt 20220521-02-04.txt 20220714-03-03.txt
...

和上一篇文章一樣,為了減少不必要的計算,以及讓展示效果更好,我們使用類似的方式對所有的內(nèi)容進行數(shù)據(jù)整理:

const { join } = require("path");
const { readdirSync, existsSync, mkdirSync, readFileSync, writeFileSync } = require("fs");

const baseDir = "./data";
const rawDir = join(baseDir, "RenMin_Daily");
const outputDir = join(baseDir, "output");

if (!existsSync(outputDir)) mkdirSync(outputDir);

const rawFiles = readdirSync("./data/RenMin_Daily").filter((file) => file.endsWith(".txt"));

let buffer = [];
let concatID = 1;
rawFiles.forEach((file, fileNo) => {
  const lines = readFileSync(join(rawDir, file), "utf-8")
    .split("\n")
    .map((line) => line.replace(/。/g, "。\n").split("\n"))
    .flat()
    .join("\n")
    .replace(/“([\S]+?)”/g, (match) => match.replace(/\n/g, ""))
    .replace(/“([\S\r\n]+?)”/g, (match) => match.replace(/[\r\n]/g, ""))
    .split("\n")
    .map((line) => line.replace(/s/g, "").trim().replace(/s/g, "—"))
    .filter((line) => line);

  buffer = buffer.concat(lines);
  if (buffer.length > 100000) {
    writeFileSync(join(outputDir, `${concatID}.txt`), buffer.slice(0, 100000).join("\n"), "utf8");
    buffer = buffer.slice(100000);
    console.log("存錢罐滿啦,換一罐繼續(xù)存 :D");
    concatID = concatID + 1;
  }
  if (fileNo === rawFiles.length - 1) {
    writeFileSync(join(outputDir, `${concatID}.txt`), buffer.join("\n"), "utf8");
    console.log("所有數(shù)據(jù)都存儲完畢了");
  }
});

使用 Node 執(zhí)行上面的程序,我們將會在 data/output 目錄中看到 14 個文本文件,除最后一個文件之外,每一個文件都包含了 10 萬行句子。對比處理前后,我們減少了接近 100MB 的文本量,能節(jié)約不少后續(xù)處理時間。

du -hs data/RenMin_Daily
282M	data/RenMin_Daily

du -hs data/output
182M	data/output

將文本數(shù)據(jù)轉(zhuǎn)換為向量數(shù)據(jù)

因為我們要處理的數(shù)據(jù)量相對比較大,處理時間會比較長,所以,我將上一篇文章中提到的處理程序,做了一些調(diào)整:

import os
from os import walk
import pandas as pd

if not os.path.exists('./data/vector'):
    os.mkdir('./data/vector')

dataDir = "./data/output"
allFiles = next(walk(dataDir), (None, None, []))[2]
# 加載原始數(shù)據(jù)
frames = []
for i in range(len(allFiles)):
    file = allFiles[i]
    print(file)
    frames.append(pd.read_csv("./data/output/"+file, sep="`",
                              header=None, names=["sentence"]))
df = pd.concat(frames, axis=0, ignore_index=True)

# 加載模型,將數(shù)據(jù)進行向量化處理
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('uer/sbert-base-chinese-nli')
sentences = df['sentence'].tolist()
sentence_embeddings = model.encode(sentences)

# 將向量處理結(jié)果存儲
import numpy as np
save_file = "data.npy"
np.save(save_file, sentence_embeddings)

file_size = os.path.getsize(save_file)
print("%7.3f MB" % (file_size/1024/1024))

程序首先會讀取目錄下所有的文件,接著使用指定的模型,將內(nèi)容轉(zhuǎn)換為向量數(shù)據(jù)。當程序?qū)⑺袛?shù)據(jù)處理為向量之后,我們將結(jié)果存儲到本地,方便后續(xù)使用。在上一篇文章中,我有詳細描述過相關(guān)內(nèi)容,如果你對向量轉(zhuǎn)換還不了解,可以先行閱讀之前的文章。將程序保存為 prepare.py,我們繼續(xù)下一步。

在準備好程序之后,我們選擇合適的設(shè)備,執(zhí)行命令從前文中提到的鏡像,創(chuàng)建一個運行容器:

docker run --rm -it faiss bash

接著,執(zhí)行我們剛剛準備好的程序文件:

python prepare.py

接下來就是漫長的等待啦,如果你希望更快的得到結(jié)果,可以嘗試使用按量付費的云主機(盡量選擇核心多一些),或者適當減少我們要進行向量化的數(shù)據(jù)條目。

在漫長的等待之后,我們將得到類似下面的輸出:

3931.465 MB

說明數(shù)據(jù)處理完畢,終于可以“上菜”了。

使用 Faiss 進行向量檢索

我們先來實現(xiàn)一段程序,來解決我們上文中提到的“搜不到內(nèi)容”的問題,比如口語化的“今天天兒怎么樣”:

# 從目錄中加載原始數(shù)據(jù)
from os import walk
import pandas as pd

dataDir = "./data/output"
allFiles = next(walk(dataDir), (None, None, []))[2]
frames = []
for i in range(len(allFiles)):
    file = allFiles[i]
    print(file)
    frames.append(pd.read_csv("./data/output/"+file, sep="`",
                              header=None, names=["sentence"]))
df = pd.concat(frames, axis=0, ignore_index=True)
print("載入原始數(shù)據(jù)完畢,數(shù)據(jù)量", len(df))

# 加載預(yù)處理數(shù)據(jù)
import numpy as np
sentences = df['sentence'].tolist()
sentence_embeddings = np.load("data.npy")
print("載入向量數(shù)據(jù)完畢,數(shù)據(jù)量", len(sentence_embeddings))

# 使用構(gòu)建向量時的模型來構(gòu)建向量索引
import faiss
dimension = sentence_embeddings.shape[1]
quantizer = faiss.IndexFlatL2(dimension)
nlist = 50
index = faiss.IndexIVFFlat(quantizer, dimension, nlist)
index.train(sentence_embeddings)
index.add(sentence_embeddings)
print("建立向量索引完畢,數(shù)據(jù)量", index.ntotal)

# 嘗試進行查詢
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('uer/sbert-base-chinese-nli')
print("載入模型完畢")

topK = 10
search = model.encode(["今天天兒怎么樣"])
D, I = index.search(search, topK)
ret = df['sentence'].iloc[I[0]]
print(ret)

上面這段程序,包含了加載原始數(shù)據(jù)和預(yù)處理好的向量數(shù)據(jù)、建立向量索引、加載模型、進行內(nèi)容檢索并輸出結(jié)果幾個向量檢索的必要步驟。在上一篇文章中,我們曾提到過建立向量索引的細節(jié):如何選擇索引、如何進行索引查詢加速等等,所以就不再贅述了,感興趣可以自行翻閱。

當我們執(zhí)行程序之后,會得到類似下面的輸出結(jié)果:

載入原始數(shù)據(jù)完畢,數(shù)據(jù)量 1341940
載入向量數(shù)據(jù)完畢,數(shù)據(jù)量 1341940
建立向量索引完畢 1341940

22966                              對預(yù)測陰晴冷暖的天氣預(yù)報,我們幾乎每天都會接觸到。
138674                                            一個多月里天天刮風。
1207740       “沙塵暴來的時候,天突然毫無征兆地昏暗下來?!遍L那么大,侯朝茹還是頭一回遭遇這么可怕的風暴。
1307072                                  天氣無常,也加大了天氣預(yù)報工作的難度。
849630                                 收到氣象災(zāi)害預(yù)警怎么辦?(把自然講給你聽)
22899      氣候預(yù)測主要是對延伸期(11天到30天)、月、季節(jié)和年度氣候趨勢進行預(yù)測,包括氣溫、降水等氣...
788733     在這千里冰封的雪域高原上,都是沙土和凍土,怎么會有綠樹扎根?在這干旱多風,5月才進春、9月就...
290305     終于結(jié)束工作,我感到一陣輕松,安多縣交警大隊隊長才加卻眉頭緊鎖——“這種天氣,我最擔心下雪。...
22884                                       從預(yù)報天氣到預(yù)測氣候(深度觀察)
783559      今年汛期的天氣氣候有什么特點?今后雨情汛情情況如何?防汛減災(zāi)應(yīng)注意哪些問題?記者采訪了多位專家。

是不是很神奇,許多結(jié)果中并沒有包含“天氣”這個關(guān)鍵詞,但是從文本描述中,我們可以比較清晰的看到,這些結(jié)果確實都在聊“天氣相關(guān)的事情”。這就是基于向量的文本檢索的強大之處。

我們再進行一個更具象問題的檢索,比如“五月份的天氣怎么樣”:

topK = 10
search = model.encode(["五月份的天氣怎么樣"])
D, I = index.search(search, topK)
ret = df['sentence'].iloc[I[0]]
print(ret)

在程序執(zhí)行完畢之后,我們將得到類似下面的結(jié)果:

725502     本報北京54日電  (記者李紅梅)中央氣象臺預(yù)計,558時至88時,北方地區(qū)將迎來大...
12507                      5月份,南方地區(qū)出現(xiàn)5次區(qū)域性暴雨過程,5月中旬以后降雨增多增強。
860001                  據(jù)介紹,從氣候分布來看,5月中下旬,華北、黃淮出現(xiàn)高溫天氣屬于正?,F(xiàn)象。
12505      本報北京69日電  (記者邱超奕)記者從應(yīng)急管理部獲悉:今年5月份,我國自然災(zāi)害以風雹、地...
722412                                   從往年5月的暴雨日看,也能驗證這句話。
411521                         今年51日入汛以來,安徽全省平均降雨量較常年同期少四成。
1300971                                5月的老撾正值熱季,地面上騰起厚重的霧氣。
847044                 5月初,貴州西北部山區(qū)飄起紛紛揚揚的雪花,烏蒙高原漫長的冬天似乎還沒過去。
873455     本報北京528日電  (記者李紅梅)528日夜間至30日,長江中下游及以南地區(qū)多降水,與...
886301                       進入農(nóng)歷五月,長江流域是梅雨季,雨多、溽熱、潮濕,易產(chǎn)生霉變。
Name: sentence, dtype: object

對于上面的檢索結(jié)果,是不是有些意外。想想一下,如果我們使用傳統(tǒng)手段來進行檢索或者識別,能否還能通過就寫這么幾行 Python 代碼來搞定呢?

當然,除了天氣,你也可以嘗試通過它來進行一些定義、觀點的檢索,比如“如何看待人工智能技術(shù)”:

topK = 10
search = model.encode(["如何看待人工智能技術(shù)"])
D, I = index.search(search, topK)
ret = df['sentence'].iloc[I[0]]
print(ret)

檢索結(jié)果會類似下面這樣:

939548                                     人工智能的出現(xiàn),有望解決這些問題。
306768                                      它是誰?它就是AI(人工智能)。
1095822                                    人工智能服務(wù)器市場中國廠商份額居前
765891                                       人臉識別是人工智能的重要應(yīng)用。
354354             高技能人才職稱怎么評?評定標準怎么定?評職稱對高技能人才有何影響?記者進行了采訪。
838729     “人工客服不能完全缺位,智能客服也不能完全取代人工客服?!痹撠撠熑吮硎?,企業(yè)不能只看到智能客...
716819                                      如何看待當前的工業(yè)經(jīng)濟運行態(tài)勢?
718423                                      如何看待當前的工業(yè)經(jīng)濟運行態(tài)勢?
337290                                    人工智能助力癌癥早期篩查(創(chuàng)新故事)
1034737                               比如人臉識別技術(shù),應(yīng)用的就是計算機視覺算法。

如果我們針對上面的內(nèi)容再次進行深加工,比如關(guān)系和實體的分析、歧義的消除,進行一些領(lǐng)域知識的建模,就能夠初步的完成一張知識圖譜啦。(或許后面有機會,我們可以展開聊聊)

好了,相信機智的你一定可以發(fā)現(xiàn),文本檢索出現(xiàn)的許多結(jié)果,如果使用上文中的“傳統(tǒng)檢索”功能,基本是搜不出來的,因為相似度并不高,或者說從字符串匹配度上來看相似度非常低。當然,結(jié)果上還存在一些比如內(nèi)容還并不夠精準的問題,需要結(jié)合一些傳統(tǒng)手段,或者對我們所使用的數(shù)據(jù)、模型進行進一步的“精耕細作”。

感受 Faiss 的檢索性能

解決完“搜不到內(nèi)容”的問題之后,我們來了解下 Faiss 的令人驚艷的性能,我們對上面的程序進行一些調(diào)整,讓內(nèi)容檢索循環(huán)進行 1000 次,然后計算它的平均值:

import time

topK = 10
search = model.encode(["如何看待人工智能技術(shù)"])

costs = []
for x in range(1000):
  t0 = time.time()
  D, I = index.search(search, topK)
  t1 = time.time()
  costs.append(t1 - t0)

print("平均耗時 %7.3f ms" % ((sum(costs) / len(costs)) * 1000.0))

程序執(zhí)行完畢后的結(jié)果:

平均耗時   5.086 ms

在沒有做任何緩存、保持對全量數(shù)據(jù)進行檢索的情況下,并使用比較慢的 Python 調(diào)用 faiss,從 134 萬長短不一的內(nèi)容中進行相似度計算,每次獲取 10 個結(jié)果,平均每次請求只用了 5ms 左右。雖然已經(jīng)達到了幾毫秒級別,但是向量檢索性能依舊存在比較大的優(yōu)化空間,至于如何在生產(chǎn)環(huán)境中優(yōu)化,我們后面的文章再慢慢聊。

其他

好啦,寫到這里,關(guān)于如何入坑向量數(shù)據(jù)庫的第二篇內(nèi)容也就基本聊完啦。

本文中除了可以使用基于 SBERT(Sentence-BERT)這種基于有監(jiān)督句向量的模型之外,還可以考慮使用 CoSENT (Cosine Sentence)。相比較前者在 huggingface 上每月只有 1 萬出頭的下載量,后者的下載量達到了驚人的每月 46 萬。

調(diào)用方式也非常簡單,只需要將本文中提到的模型加載相關(guān)代碼中的模型名稱進行替換即可,比如:

model = SentenceTransformer('uer/sbert-base-chinese-nli')
# 替換為
model = SentenceTransformer('shibing624/text2vec-base-chinese')

雖然向量相似度檢索技術(shù)在文本檢索方面有“奇效”,但是相對于傳統(tǒng)檢索手段而言,依舊存在著一些問題:模型的創(chuàng)造、使用都存在額外的計算成本,ETL 過程需要花費不少的時間,客戶端語言性能也有比較大的提升空間,檢索方面的調(diào)試目前還比較黑盒,以及想要在生產(chǎn)中“多快好省”,“穩(wěn)定靠譜”,還需要額外的努力。

畢竟,我們都知道,這個世界上沒有銀彈。在許多場景下,它也并非是現(xiàn)有手段的完全替代,而是一顆不斷進化的增強裂變彈。

最后

希望在看完這篇內(nèi)容后,你對于傳統(tǒng)文本檢索與基于向量的語義檢索會有一個相對立體的認知,能夠熟悉這其中最常見的“套路”,并且了解到相似度檢索技術(shù),它到底解決了什么問題,我們在什么場景下需要借助“向量檢索的能力”來解決問題。

當然,如果你希望像使用 MySQL 一樣,向量索引都由軟件自己個兒解決,并且有比較大規(guī)模的數(shù)據(jù)需要處理,那么依舊推薦你直接使用封裝了 Faiss 的 Milvus ,或者更簡單一些,直接使用 Milvus 的 Cloud 服務(wù)咯。

雖然寫了這么多,但是關(guān)于文本檢索還有很多沒有聊到的問題,希望在后面的內(nèi)容中,我們可以繼續(xù)聊完這個話題。

祝大家,中秋快樂。

–EOF


本文使用「署名 4.0 國際 (CC BY 4.0)」許可協(xié)議,歡迎轉(zhuǎn)載、或重新修改使用,但需要注明來源。 署名 4.0 國際 (CC BY 4.0)

本文作者: 蘇洋

創(chuàng)建時間: 2022年09月10日
統(tǒng)計字數(shù): 26417字
閱讀時間: 53分鐘閱讀
本文鏈接: https://soulteary.io/2022/09/10/the-dimensionality-reduction-of-traditional-text-retrieval-methods-using-faiss-to-achieve-vector-semantic-retrieval.html文章來源地址http://www.zghlxwxcb.cn/news/detail-595512.html

到了這里,關(guān)于向量數(shù)據(jù)庫入坑:傳統(tǒng)文本檢索方式的降維打擊,使用 Faiss 實現(xiàn)向量語義檢索的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔相關(guān)法律責任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費用

相關(guān)文章

  • 向量數(shù)據(jù)庫:usearch的簡單使用+實現(xiàn)圖片檢索應(yīng)用

    向量數(shù)據(jù)庫:usearch的簡單使用+實現(xiàn)圖片檢索應(yīng)用

    usearch是快速開源搜索和聚類引擎×,用于C++、C、Python、JavaScript、Rust、Java、Objective-C、Swift、C#、GoLang和Wolfram ??中的向量和??字符串× 一個簡單的例子(注:本例子在運行時向index中不斷添加項目,并將最后的index持久化為一個文件,在運行時由于添加項目內(nèi)存占用會不斷增

    2024年02月02日
    瀏覽(98)
  • ModaHub魔搭社區(qū)——未來向量數(shù)據(jù)庫會不像傳統(tǒng)數(shù)據(jù)庫那樣,在國內(nèi)涌現(xiàn) 200 多家出來?

    ModaHub魔搭社區(qū)——未來向量數(shù)據(jù)庫會不像傳統(tǒng)數(shù)據(jù)庫那樣,在國內(nèi)涌現(xiàn) 200 多家出來?

    隨著技術(shù)的迭代速度越來越快,技術(shù)門檻也在逐漸降低,數(shù)據(jù)庫市場的持續(xù)擴張是不可避免的。當前存在著大量的需求,這將吸引越來越多的數(shù)據(jù)庫甚至向量數(shù)據(jù)庫加入競爭。然而,從業(yè)界角度看,這種市場擴張是有利的。它可以促使更多的技術(shù)和業(yè)務(wù)參與,盡管市場在一定

    2024年02月10日
    瀏覽(34)
  • Spring AI - 使用向量數(shù)據(jù)庫實現(xiàn)檢索式AI對話

    Spring AI - 使用向量數(shù)據(jù)庫實現(xiàn)檢索式AI對話

    ?Spring AI 并不僅限于針對大語言模型對話API進行了統(tǒng)一封裝,它還可以通過簡單的方式實現(xiàn)LangChain的一些功能。本篇將帶領(lǐng)讀者實現(xiàn)一個簡單的檢索式AI對話接口。 ?在一些場景下,我們想讓AI根據(jù)我們提供的數(shù)據(jù)進行回復(fù)。因為對話有最大Token的限制,因此很多場景下我們

    2024年04月14日
    瀏覽(94)
  • 從零開始構(gòu)建基于milvus向量數(shù)據(jù)庫的文本搜索引擎

    從零開始構(gòu)建基于milvus向量數(shù)據(jù)庫的文本搜索引擎

    在這篇文章中,我們將手動構(gòu)建一個語義相似性搜索引擎,該引擎將單個論文作為“查詢”輸入,并查找Top-K的最類似論文。主要包括以下內(nèi)容: 1.搭建milvus矢量數(shù)據(jù)庫 2.使用MILVUS矢量數(shù)據(jù)庫搭建語義相似性搜索引擎 3.從Kaggle下載ARXIV數(shù)據(jù),使用dask將數(shù)據(jù)加載到Python中,并構(gòu)

    2024年02月09日
    瀏覽(25)
  • 理解構(gòu)建LLM驅(qū)動的聊天機器人時的向量數(shù)據(jù)庫檢索的局限性 - (第1/3部分)

    理解構(gòu)建LLM驅(qū)動的聊天機器人時的向量數(shù)據(jù)庫檢索的局限性 - (第1/3部分)

    本博客是一系列文章中的第一篇,解釋了為什么使用大型語言模型( LLM )部署專用領(lǐng)域聊天機器人的主流管道成本太高且效率低下。在第一篇文章中,我們將討論為什么矢量數(shù)據(jù)庫盡管最近流行起來,但在實際生產(chǎn)管道中部署時從根本上受到限制。在下面的文章中,我們說

    2024年02月14日
    瀏覽(22)
  • AI實踐與學習1_NLP文本特征提取以及Milvus向量數(shù)據(jù)庫實踐

    AI實踐與學習1_NLP文本特征提取以及Milvus向量數(shù)據(jù)庫實踐

    隨著NLP預(yù)訓練模型(大模型)以及多模態(tài)研究領(lǐng)域的發(fā)展,向量數(shù)據(jù)庫被使用的越來越多。 在XOP億級題庫業(yè)務(wù)背景下,對于試題召回搜索單單靠著ES分片集群普通搜索已經(jīng)出現(xiàn)性能瓶頸,因此需要預(yù)研其他技術(shù)方案提高試題搜索召回率。 現(xiàn)一個方案就是使用Bert等模型提取試

    2024年01月24日
    瀏覽(48)
  • LangChain 4用向量數(shù)據(jù)庫Faiss存儲,讀取YouTube的視頻文本搜索Indexes for information retrieve

    LangChain 4用向量數(shù)據(jù)庫Faiss存儲,讀取YouTube的視頻文本搜索Indexes for information retrieve

    接著前面的Langchain,繼續(xù)實現(xiàn)讀取YouTube的視頻腳本來問答Indexes for information retrieve LangChain 實現(xiàn)給動物取名字, LangChain 2模塊化prompt template并用streamlit生成網(wǎng)站 實現(xiàn)給動物取名字 LangChain 3使用Agent訪問Wikipedia和llm-math計算狗的平均年齡 引用向量數(shù)據(jù)庫Faiss 查看OpenAI model main.p

    2024年02月05日
    瀏覽(31)
  • 【數(shù)據(jù)庫原理 | MySQL】 前世今生(入坑篇)

    【數(shù)據(jù)庫原理 | MySQL】 前世今生(入坑篇)

    ???♂? 個人主頁: @計算機魔術(shù)師 ????? 作者簡介:CSDN內(nèi)容合伙人,全棧領(lǐng)域優(yōu)質(zhì)創(chuàng)作者。 我們先闡述如下概念 名稱 全稱 作用 數(shù)據(jù)庫 存貯數(shù)據(jù)的倉庫,數(shù)據(jù)是有組織的存貯 DataBase(DB) 數(shù)據(jù)庫管理系統(tǒng) 操作和管理數(shù)據(jù)庫的大型軟件 DataBase Management System(DBMS SQL 一套專門

    2024年01月16日
    瀏覽(23)
  • 什么是向量數(shù)據(jù)庫?向量數(shù)據(jù)庫工作原理?向量數(shù)據(jù)庫解決方案?

    什么是向量數(shù)據(jù)庫?向量數(shù)據(jù)庫工作原理?向量數(shù)據(jù)庫解決方案?

    向量數(shù)據(jù)庫是一種專門用于存儲和處理向量數(shù)據(jù)的數(shù)據(jù)庫系統(tǒng)。向量數(shù)據(jù)是指具有多維度屬性的數(shù)據(jù),例如圖片、音頻、視頻、自然語言文本等。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫通常不擅長處理向量數(shù)據(jù),因為它們需要將數(shù)據(jù)映射成結(jié)構(gòu)化的表格形式,而向量數(shù)據(jù)的維度較高、結(jié)構(gòu)復(fù)雜

    2024年02月15日
    瀏覽(38)
  • 傳統(tǒng)數(shù)據(jù)庫逐漸“難適應(yīng)”,云原生數(shù)據(jù)庫脫穎而出

    傳統(tǒng)數(shù)據(jù)庫逐漸“難適應(yīng)”,云原生數(shù)據(jù)庫脫穎而出

    數(shù)據(jù)庫一直是應(yīng)用開發(fā)中非常重要的一部分。從MySQL到亞馬遜的RDS(關(guān)系型數(shù)據(jù)庫服務(wù),Relational Database Service),業(yè)界有很多數(shù)據(jù)庫系統(tǒng)供開發(fā)者存儲、查詢和管理數(shù)據(jù)。隨著海量計算的持續(xù)發(fā)展,給傳統(tǒng)數(shù)據(jù)庫帶來了不少挑戰(zhàn),而云原生數(shù)據(jù)庫卻可以應(yīng)對這些挑戰(zhàn)。 亞馬遜云

    2024年01月22日
    瀏覽(26)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包