向量數據庫和嵌入模型

關注 AI、大模型的朋友們應該都聽過用 RAG(Retrieval-augmented Generation,檢索增強生成)搭建知識庫,RAG 的基礎組成之二就是向量數據庫和嵌入模型。在正式閱讀本文前,我們先來看幾個問題:

1、什麼是向量?

2、什麼是文本 / 圖片向量化?

3、什麼是向量數據庫?有哪些向量數據庫?

4、將數據保存進向量數據庫後,該如何檢索查詢?

5、向量數據庫和 Mysql 的查詢與什麼區別和不同?

向量數據庫

Vector 是向量或矢量的意思,向量是數學裏的概念,而矢量是物理裏的概念,但二者描述的是同一件事。向量的準確定義:向量是用於表示具有大小和方向的量。具體而言,向量可以在不同的維度空間中定義,最常見的是二維和三維空間中的向量,但理論上也可以有更高維的向量。例如,在二維平面上的一個向量可以寫作(x,y),這裏 x 和 y 分別表示該向量沿兩個座標軸方向上的分量;而在三維空間裏,則會有一個額外的 z 座標,即(x,y,z)。

如我們所見,每個數值向量都有 x 和 y 座標(或者在多維繫統中是 x、y、z,...)。x、y、z... 是這個向量空間的軸,稱爲維度。對於我們想要表示爲向量的一些非數值實體,我們首先需要決定這些維度,併爲每個實體在每個維度上分配一個值。

例如,在一個交通工具數據集中,我們可以定義四個維度:“輪子數量”、“是否有發動機”、“是否可以在地上開動” 和 “最大乘客數”。然後我們可以將一些車輛表示爲:

因此,我們的汽車 Car 向量將是 (4, yes, yes, 5),或者用數值表示爲 (4, 1, 1, 5)(將 yes 設爲 1,no 設爲 0)。向量的每個維度代表數據的不同特性,維度越多對事務的描述越精確,我們可以使用 “是否有翅膀”、“是否使用柴油”、“最高速度”、“平均重量”、“價格” 等等更多的維度信息。

每個向量都有一個長度和方向。例如,在這個圖中,p 和 a 指向相同的方向,但長度不同。p 和 b 正好指向相反的方向,但有相同的長度。然後還有 c,長度比 p 短一點,方向不完全相同,但很接近。

那麼,哪一個最接近 p 呢?

如果 “相似” 僅僅意味着指向相似的方向,那麼 a 是最接近 p 的。接下來是 c。b 是最不相似的,因爲它正好指向與 p 相反的方向。如果 “相似” 僅僅意味着相似的長度,那麼 b 是最接近 p 的(因爲它有相同的長度), 接下來是 c,然後是 a。

由於向量通常用於描述語義意義,僅僅看長度通常無法滿足需求。大多數相似度測量要麼僅依賴於方向,要麼同時考慮方向和大小。

在數學中向量間的相似度測量即相似度計算,有四種常見的計算方法,這裏不展開討論。

向量存儲(Vector Database/VectorStore)是一種用於存儲和檢索高維向量數據的數據庫或存儲解決方案,它特別適用於處理那些經過嵌入模型轉化後的數據。在向量數據庫中,查詢與傳統關係數據庫不同,它們執行相似性搜索,而不是精確匹配。當給定一個向量作爲查詢時,VectorStore 返回與查詢向量 “相似 " 的向量。比如說在使用一個商城系統的向量數據庫進行查詢的時候,用戶輸入“北京”,其可能返回的結果會是 “中國、北京、華北、首都、奧運會” 等信息;輸入“瀋陽”,其返回結果可能會是“東北、遼寧、雪花、重工業” 等信息。當然,返回的信息取決於向量數據庫中存在的數據。用戶可以通過參數的設置來限定返回的情況,進而適配不同的需求。

向量數據庫用於將您的數據與 Al 模型集成。在使用它們時的第一步是將您的數據加載到向量數據庫中。然後,當要將用戶查詢發送到 AI 模型時,首先檢索一組相似文檔。然後,這些文檔作爲用戶問題的上下文,並與用戶的查詢一起發送到 Al 模型。這種技術被稱爲檢索增強生(RetrievalAugmentedGeneration, RAG )。

說人話核心就是將文本、圖像和視頻等等轉換爲一組浮點數數組(即向量)後進行存儲的系統就是向量數據庫,其查詢與傳統關係數據庫不同,向量數據庫執行相似性搜索,而不是精確匹配。當給定一個向量作爲查詢時,向量數據庫返回與查詢向量 “相似 " 的向量。

能做向量數據庫有如下產品

_上圖源於:_https://docs.langchain4j.dev/integrations/embedding-stores/

嵌入模型

嵌入模型(Embedding Model)和向量數據庫(Vector Database/Vector Store)是一對親密無間的合作伙伴,也是 AI 技術棧中緊密關聯的兩大核心組件,兩者的協同作用構成了現代語義搜索、推薦系統和 RAG(Retrieval Augmented Generation,檢索增強生成)等應用的技術基礎。

嵌入(Embedding)的工作原理是將文本、圖像和視頻轉換爲稱爲向量(Vectors)的浮點數數組。這些向量旨在捕捉文本、圖像和視頻的含義。嵌入數組的長度稱爲向量的維度(Dimensionality)。

嵌入模型是嵌入過程中採用的模型。當前 EmbeddingModel 的接口主要用於將文本轉換爲數值向量,接口的設計主要圍繞這兩個目標展開:

嵌入模型是一種機器學習模型,旨在在連續的低維向量空間中表示數據(例如文本、圖像或其他形式的信息)。這些嵌入可以捕獲數據之間的語義或上下文相似性,使機器能夠更有效地執行比較、聚類或分類等任務。

假設你想描述不同的水果。你不用長篇大論,而是用數字來描述甜度、大小和顏色等特徵。例如,蘋果可能是 [8,5.7],而香蕉是 [9,7,4]。這些數字使比較或對相似的水果進行分組變得更容易。

代碼演示

上述講了理論概念,沒有實際操作會不夠具象化理解,嵌入模型選擇阿里百鍊平臺的通用文本向量 - v3(text-embedding-v3),當然你也可以選擇其他平臺的嵌入模型,如 BAAI/bge-m3 等

向量數據庫選擇 Qdrant,使用 Docker 容器方式運行:

docker run -d  -p 6333:6333 -p 6334:6334 qdrant/qdrant 
#運行後瀏覽器訪問http://localhost:6333/dashboard#/welcome 進入可視化頁面

將吳青峯演唱的《我們都擁有海洋》歌詞通過 text-embedding-v3 嵌入模型(1024 維)向量化後存入 Qdrant

最後,感謝您的閱讀!系列文章會同步更新在微信公衆號 @雲上的喵醬、阿里雲開發者社區 @雲上的喵醬、CSDN@笠泱,您的點贊 + 關注 + 轉發是我後續更新的動力!

附錄:lanchain4j 代碼實現

EmbeddingModelConfig

package com.liyang.study.config;

import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.qdrant.QdrantEmbeddingStore;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @auther liyang
 * @create 2025-05-28 00:00
 */
@Configuration(proxyBeanMethods = false)
public class EmbeddingModelConfig
{
    @Bean
    public EmbeddingModel embeddingModel()  //EmbeddingModel 向量接口
    {
        return OpenAiEmbeddingModel.builder()
        .apiKey(System.getenv("LANGCHAIN4J_KEY"))
        .modelName("text-embedding-v3")
        .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
        //              .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings")
        .build();
    }

    @Bean
    public QdrantClient qdrantClient() {
        QdrantGrpcClient.Builder grpcClientBuilder =
        QdrantGrpcClient.newBuilder("127.0.0.1", 6334, false);
        return new QdrantClient(grpcClientBuilder.build());
    }

    @Bean
    public EmbeddingStore<TextSegment> embeddingStore() {
        return QdrantEmbeddingStore.builder()
        .host("127.0.0.1")
        .port(6334)
        .collectionName("test-qdrant")
        .build();
    }
}

EmbeddinglController

package com.liyang.study.controller;

import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingSearchResult;
import dev.langchain4j.store.embedding.EmbeddingStore;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.grpc.Collections;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import dev.langchain4j.model.embedding.EmbeddingModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import dev.langchain4j.data.embedding.Embedding;

import java.util.Vector;

import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey;

/**
 * @auther liyang
 * @create 2025-05-28 00:00
 */
@RestController
@Slf4j
public class EmbeddinglController
{
    @Resource
    private EmbeddingModel embeddingModel;
    @Resource
    private QdrantClient qdrantClient;
    @Resource
    private EmbeddingStore<TextSegment> embeddingStore;

    /**
     * 文本向量化測試,看看形成向量後的文本
     * http://localhost:9012/embedding/embed
     * @return
     */
    @GetMapping(value = "/embedding/embed")
    public String embed()
    {
        String prompt = """
                我們都擁有海洋的歌詞如下:
                如果這是 再不返回的夏天
                好好的告別 和重要的人說再見
                無關那些 催人啓程的祝願
                這世界太吵 不代表你的聲音不珍貴
                去哪 讓我選擇
                如果 只有岸沒有海
                那我 長奔跑的腳踝
                到底是爲什麼
                爲劇烈的心動
                跑起來去見 喜歡的我
                興趣使然的
                怎麼判斷有用 誰來規定無用
                完美的旅程
                爲何每一站 是一樣的路牌
                沒有 石頭 所以 所以 沒有 悟空
                就讓我自己走吧
                穿着 球鞋 我的球鞋
                迎着 暴雪 我的暴雪
                朝着 山野 要去見
                所有我喜歡的一切
                就讓我自己走吧
                哪一代 什麼浪 都不是 我的模樣
                別對我說 去成爲誰的模樣
                未來不在 沙灘上 我本該擁有海洋
                我本該擁有海洋 就讓我自己走吧
                孤島極光 虛擬城邦 星球流浪
                不管會遇見什麼 hello hello hello
                你可見過 兩片相同的樹葉
                人不可複寫 生命是原創的情節
                去問明天 渴望怎樣一張臉
                它說是少年 它說和我一樣毫無經驗
                不懂 天高地厚
                是否 是短暫的自由
                多久 像風箏愛天空
                怎能害怕 墜落
                人生中的考場
                我會勇敢地 給出回答
                親手署名吧
                有時也會考砸 不抄誰的答案
                揮手告別吧
                記得二十歲 流着淚的笑臉
                前路 卡關 借來 借來 加血 加防
                就讓我自己走吧
                穿着 球鞋 我的球鞋
                迎着 暴雪 我的暴雪
                朝着 山野 要去見
                所有我喜歡的一切
                就讓我自己走吧
                哪一代 什麼浪 都不是 我的模樣
                別對我說 去成爲誰的模樣
                未來不在 沙灘上 我本該擁有海洋
                我本該擁有海洋 就讓我自己走吧
                孤島極光 虛擬城邦 星球流浪
                我將走向 無垠遠方
                我本該擁有海洋 就讓我自己走吧
                我不遺忘 我的模樣
                不管答案是什麼 hello hello hello
                我們都擁有海洋
                """;
        Response<Embedding> embeddingResponse = embeddingModel.embed(prompt);

        System.out.println(embeddingResponse);

        return embeddingResponse.content().toString();
    }

    /** http://localhost:9012/embedding/createCollection
     * 新建向量數據庫實例和創建索引:test-qdrant
     * 類似mysql create database test-qdrant
     */
    @GetMapping(value = "/embedding/createCollection")
    public void createCollection()
    {
        var vectorParams = Collections.VectorParams.newBuilder()
                .setDistance(Collections.Distance.Cosine)
                .setSize(1024)
                .build();
        qdrantClient.createCollectionAsync("test-qdrant", vectorParams);
    }

    /*
    http://localhost:9012/embedding/add
     往向量數據庫新增文本記錄
     */
    @GetMapping(value = "/embedding/add")
    public String add()
    {
        String prompt = """
                我們都擁有海洋的歌詞如下:
                如果這是 再不返回的夏天
                好好的告別 和重要的人說再見
                無關那些 催人啓程的祝願
                這世界太吵 不代表你的聲音不珍貴
                去哪 讓我選擇
                如果 只有岸沒有海
                那我 長奔跑的腳踝
                到底是爲什麼
                爲劇烈的心動
                跑起來去見 喜歡的我
                興趣使然的
                怎麼判斷有用 誰來規定無用
                完美的旅程
                爲何每一站 是一樣的路牌
                沒有 石頭 所以 所以 沒有 悟空
                就讓我自己走吧
                穿着 球鞋 我的球鞋
                迎着 暴雪 我的暴雪
                朝着 山野 要去見
                所有我喜歡的一切
                就讓我自己走吧
                哪一代 什麼浪 都不是 我的模樣
                別對我說 去成爲誰的模樣
                未來不在 沙灘上 我本該擁有海洋
                我本該擁有海洋 就讓我自己走吧
                孤島極光 虛擬城邦 星球流浪
                不管會遇見什麼 hello hello hello
                你可見過 兩片相同的樹葉
                人不可複寫 生命是原創的情節
                去問明天 渴望怎樣一張臉
                它說是少年 它說和我一樣毫無經驗
                不懂 天高地厚
                是否 是短暫的自由
                多久 像風箏愛天空
                怎能害怕 墜落
                人生中的考場
                我會勇敢地 給出回答
                親手署名吧
                有時也會考砸 不抄誰的答案
                揮手告別吧
                記得二十歲 流着淚的笑臉
                前路 卡關 借來 借來 加血 加防
                就讓我自己走吧
                穿着 球鞋 我的球鞋
                迎着 暴雪 我的暴雪
                朝着 山野 要去見
                所有我喜歡的一切
                就讓我自己走吧
                哪一代 什麼浪 都不是 我的模樣
                別對我說 去成爲誰的模樣
                未來不在 沙灘上 我本該擁有海洋
                我本該擁有海洋 就讓我自己走吧
                孤島極光 虛擬城邦 星球流浪
                我將走向 無垠遠方
                我本該擁有海洋 就讓我自己走吧
                我不遺忘 我的模樣
                不管答案是什麼 hello hello hello
                我們都擁有海洋
                """;
        TextSegment segment1 = TextSegment.from(prompt);
        segment1.metadata().put("author", "吳青峯");
        segment1.metadata().put("price", "priceless");
        Embedding embedding1 = embeddingModel.embed(segment1).content();
        String result = embeddingStore.add(embedding1, segment1);

        System.out.println(result);

        return result;
    }

    @GetMapping(value = "/embedding/query1")
    public void query1(){
        Embedding queryEmbedding = embeddingModel.embed("我們都擁有海洋歌詞說了什麼").content();
        EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
                .queryEmbedding(queryEmbedding)
                .maxResults(1)
                .build();
        EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(embeddingSearchRequest);
        System.out.println(searchResult.matches().get(0).embedded().text());
    }

    @GetMapping(value = "/embedding/query2")
    public void query2(){
        Embedding queryEmbedding = embeddingModel.embed("我們都擁有海洋的演唱者是誰").content();
        EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
                .queryEmbedding(queryEmbedding)
                .filter(metadataKey("author").isEqualTo("吳青峯"))
                .maxResults(1)
                .build();

        EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(embeddingSearchRequest);


        System.out.println(searchResult.matches().get(0).embedded().text());
    }
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/DEDonnt720kOSJo4Lj6_UQ