go-redis 使用RedisSearch進(jìn)行向量的檢索與查詢

Redis 允許您在哈希JSON對象中索引向量字段(更多信息請參閱向量參考頁面)。向量字段可以存儲(chǔ)文本embedding等內(nèi)容,文本embedding是 AI 生成的向量表示,用于表示文本片段中的語義信息。兩個(gè)embedding之間的向量距離表明它們在語義上的相似程度。通過比較從查詢文本生成的embedding與存儲(chǔ)在哈?;?JSON 字段中的embedding的相似性,Redis 可以檢索與查詢的含義密切相關(guān)的文檔。

創(chuàng)建索引


func doCreateIndex() (err error) {
    var (
        RedisKeyPrefix = "doc:"
        IndexName      = "vactor_test"
        Dimension      = 2560 //| 實(shí)際字節(jié)數(shù) | 你傳進(jìn)來的 blob 長度 | 10240 字節(jié) | → 10240 ÷ 4 = 2560 維
    )
    cli := NewRedisStackClient("localhost:6379", "", 0)
    ctx := context.Background()
    // 確保在錯(cuò)誤時(shí)關(guān)閉連接
    defer func() {
        if err != nil {
            cli.Client.Close()
        }
    }()
    if err = cli.Client.Ping(ctx).Err(); err != nil {
        return fmt.Errorf("failed to connect to Redis: %w", err)
    }
    indexName := fmt.Sprintf("%s%s", RedisKeyPrefix, IndexName)
    // 檢查是否存在索引
    exists, err := cli.Client.Do(ctx, "FT.INFO", indexName).Result()
    if err != nil {
        if !strings.Contains(err.Error(), "Unknown index name") {
            return fmt.Errorf("failed to check if index exists: %w", err)
        }
        err = nil
    } else if exists != nil {
        return nil
    }
    // Create new index
    createIndexArgs := []interface{}{
        "FT.CREATE", indexName,
        "ON", "HASH", //-- 數(shù)據(jù)載體:JSON 或 HASH
        "PREFIX", "1", RedisKeyPrefix, // -- 只掃描以 doc: 開頭的鍵
        "SCHEMA",
        "content", "TEXT", //-- 業(yè)務(wù)字段示例content, 文本類型
        "genre", "TAG", //-- 業(yè)務(wù)字段示例metadata, TAG類型
        "embedding", "VECTOR", "FLAT", //-- 業(yè)務(wù)字段示例vector, 向量類型
        "6",               //-- 6 = 接下來 6 個(gè)參數(shù)
        "TYPE", "FLOAT32", //向量元素類型
        "DIM", Dimension, //向量維度,與模型一致
        "DISTANCE_METRIC", "COSINE", //距離算法:COSINE / L2 / IP
    }
    if err = cli.Client.Do(ctx, createIndexArgs...).Err(); err != nil {
        return fmt.Errorf("failed to create index: %w", err)
    }
    // 驗(yàn)證索引是否創(chuàng)建成功
    if _, err = cli.Client.Do(ctx, "FT.INFO", indexName).Result(); err != nil {
        return fmt.Errorf("failed to verify index creation: %w", err)
    }
    return nil
}

索引:doc:vactor_test只掃描以 doc: 開頭的鍵 , 包含三個(gè)字段:

  • content: 內(nèi)容, TEXT文本類型
  • genre: 類型, TAG標(biāo)簽類型
  • embedding:向量, VECTOR 類型,F(xiàn)LAT標(biāo)識(shí)向量檢索的方式(HNSW 適合在線、高并發(fā)近似搜索;若數(shù)據(jù)量很小可用 FLAT(暴力線性掃描)), FLAT的原理: 不做任何近似或聚類(與 HNSW / IVF 不同)。每次 FT.SEARCH … KNN 都把查詢向量與索引中的 每一條向量 計(jì)算距離,再排序取 Top-k。因此 內(nèi)存占用高(需要完整存儲(chǔ)原始向量),但 召回率 100 %,也無額外調(diào)參。適用場景為數(shù)據(jù)量 ≤ 幾萬條或延遲要求不高。

DIM , 標(biāo)識(shí)向量維度, 必須與模型一直, 這里使用的是字節(jié)ARK的模型,維度為 2560, 在 給 RediSearch 建索引時(shí),你需把 DIM 設(shè)成實(shí)際要用的那個(gè)值, 在使用時(shí),因一開始不確定維度數(shù),設(shè)置的值384, 系統(tǒng)報(bào)以下錯(cuò)誤:

Could not add vector with blob size 10240 (expected size 1536)"

期望字節(jié)數(shù): DIM × 4, 即 384 × 4 = 1536 字節(jié), 但實(shí)際ARK返回的向量維度為10240 * 4 = 2560(除4是因?yàn)閒loat32 占4字節(jié)), 故刪除索引后重建索引, 修改DIM值為2560, 上述問題解決, 刪除索引:

func dropIndex() error {
    cli := NewRedisStackClient("localhost:6379", "", 0)
    ctx := context.Background()
    return cli.Client.FTDropIndex(ctx, "doc:vactor_test").Err()
}

DISTANCE_METRIC 用來告訴 RediSearch 向量之間的距離如何計(jì)算。目前支持三種,選其一即可:

取值 全稱 公式(簡化) 適用場景
COSINE Cosine Similarity 1 ? cos(θ) 文本 / 語義向量,長度已歸一化
L2 Euclidean Distance √Σ(xi ? yi)2 通用數(shù)值向量,維度量綱一致
IP Inner Product ? Σ(xi·yi) 已歸一化且需要最大化內(nèi)積

基于字節(jié)ARK的embedding 模型對文本向量化,并存在在redis中


func addEmbeddins() (err error) {
    cli := NewRedisStackClient("localhost:6379", "", 0)
    ctx := context.Background()
    // 確保在錯(cuò)誤時(shí)關(guān)閉連接
    defer func() {
        if err != nil {
            cli.Client.Close()
        }
    }()
    sentences := []string{
        "That is a very happy person",
        "That is a happy dog",
        "Today is a sunny day",
    }
    tags := []string{
        "persons", "pets", "weather",
    }
    config := &ark.EmbeddingConfig{
        Model:  os.Getenv("ARK_EMBEDDING_MODEL"),
        APIKey: os.Getenv("ARK_API_KEY"),
    }
    eb, err := ark.NewEmbedder(ctx, config)
    if err != nil {
        return err
    }
    embeddings, err := eb.EmbedStrings(ctx, sentences)
    if err != nil {
        return err
    }
    for i, emb := range embeddings {
        buffer := utils.Vector2Bytes(emb)
        if err != nil {
            return err
        }
        count, err := cli.Client.HSet(ctx,
            fmt.Sprintf("doc:%v", i),
            map[string]any{
                "content":   sentences[i],
                "genre":     tags[i],
                "embedding": buffer,
            },
        ).Result()
        if err != nil {
            return err
        }
    }
    return nil
}

字節(jié)開源的大模型應(yīng)用框架eino-ext 已封裝Ark了對應(yīng)的Embedder組件, 用于文檔的向量化,需引入依賴: github.com/cloudwego/eino-ext/components/embedding/ark
Embedder組件的EmbedStrings方法,入?yún)閟tring類型的切片, 出參為一個(gè)float64類型的二維數(shù)組, 對應(yīng)每個(gè)字符串向量值, 存儲(chǔ)時(shí)需要將float64轉(zhuǎn)為float32 后再轉(zhuǎn)bytes 切片存儲(chǔ):

func Vector2Bytes(vector []float64) []byte {
    float32Arr := make([]float32, len(vector))
    for i, v := range vector {
        float32Arr[i] = float32(v)
    }
    bytes := make([]byte, len(float32Arr)*4)
    for i, v := range float32Arr {
        binary.LittleEndian.PutUint32(bytes[i*4:], math.Float32bits(v))
    }
    return bytes
}

存入Redis 中的數(shù)據(jù)如下圖所示:

image.png

向量查詢

func doVectorQuery() (err error) {
    var (
        RedisKeyPrefix = "doc:"
        IndexName      = "vactor_test"
        queryStr = []string{"That is a happy person"}
    )
    cli := NewRedisStackClient("localhost:6379", "", 0)
    ctx := context.Background()
    config := &ark.EmbeddingConfig{
        Model:  os.Getenv("ARK_EMBEDDING_MODEL"),
        APIKey: os.Getenv("ARK_API_KEY"),
    }
    eb, err := ark.NewEmbedder(ctx, config)
    if err != nil {
        return err
    }
    embeddings, err := eb.EmbedStrings(ctx, queryStr)
    if err != nil {
        panic(err)
    }
    buffer := utils.FloatsToBytes(utils.ToFloat32(embeddings[0]))
    if err != nil {
        panic(err)
    }
    indexName := fmt.Sprintf("%s%s", RedisKeyPrefix, IndexName)

    //*=>[ ... ]
    //RediSearch 的 “向量查詢子句” 語法糖,* 代表“所有文檔”,=> 后面放向量運(yùn)算。
    //KNN 3
    //取 3 個(gè)最近鄰(k-nearest-neighbors)。
    //@embedding
    //指定 要比較的向量字段(索引里必須事先聲明為 VECTOR 類型)。
    //$vec
    //用戶傳入的查詢向量,需在 PARAMS 里綁定,例如 PARAMS 2 vec <base64或float數(shù)組>。
    //AS vector_distance
    //把計(jì)算出的距離(或相似度)作為 返回字段,后續(xù)可在 RETURN / SORTBY / DIALECT 里引用。
    results, err := cli.Client.FTSearchWithArgs(ctx,
        indexName,
        "*=>[KNN 3 @embedding $vec AS vector_distance]",
        &redis.FTSearchOptions{
            Return: []redis.FTSearchReturn{
                {FieldName: "vector_distance"},
                {FieldName: "content"},
            },
            DialectVersion: 2,
            Params: map[string]any{
                "vec": buffer,
            },
        },
    ).Result()

    if err != nil {
        panic(err)
    }

    for _, doc := range results.Docs {
        fmt.Printf(
            "ID: %v, Distance:%v, Content:'%v'\n",
            doc.ID, doc.Fields["vector_distance"], doc.Fields["content"],
        )
    }
    return nil
}

向量查詢語句That is a happy person, 同樣需要使用的Ark的Embedder組件進(jìn)行向量化, 將Floate64類型轉(zhuǎn)為float32類型后轉(zhuǎn)bytes切片, 進(jìn)行匹配查詢:

    results, err := cli.Client.FTSearchWithArgs(ctx,
        indexName,
        "*=>[KNN 3 @embedding $vec AS vector_distance]",
        &redis.FTSearchOptions{
            Return: []redis.FTSearchReturn{
                {FieldName: "vector_distance"},
                {FieldName: "content"},
            },
            DialectVersion: 2,
            Params: map[string]any{
                "vec": buffer,
            },
        },
    ).Result()
  • =>[ ... ]
    RediSearch 的 “向量查詢子句” 語法糖,
    代表“所有文檔”,=> 后面放向量運(yùn)算。
  • KNN 3
    取 3 個(gè)最近鄰(k-nearest-neighbors)。
  • @embedding
    指定 要比較的向量字段(索引里必須事先聲明為 VECTOR 類型)。
  • $vec
    用戶傳入的查詢向量,需在 Params里綁定, 統(tǒng)一放入到map里。
  • AS vector_distance
    把計(jì)算出的距離(或相似度)作為 返回字段,后續(xù)可在 RETURN / SORTBY / DIALECT 里引用。

上述查詢語句返回除ID之外聲明的vector_distance和content字段, DialectVersion(在 Redis/RediSearch 中常寫作 DIALECT)是 查詢語法版本號(hào),用來告訴 RediSearch 用哪一套解析器去解釋你的 FT.SEARCH、FT.AGGREGATE 等命令。向量檢索、參數(shù)綁定、JSON 多值字段 必須 DIALECT ≥ 2,否則會(huì)報(bào)錯(cuò) “syntax error”。

That is a happy person 語句的查詢返回輸出:


image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容