如何優(yōu)化表格存儲(chǔ)的向量檢索效果
表格存儲(chǔ)基于多元索引提供了向量檢索的能力,可以在大規(guī)模數(shù)據(jù)集中找到最相似的數(shù)據(jù)項(xiàng)。如果您在使用向量檢索進(jìn)行語(yǔ)義搜索時(shí)的檢索效果不符合預(yù)期,請(qǐng)按照本文的排查思路進(jìn)行向量檢索優(yōu)化。
向量檢索評(píng)分公式
表格存儲(chǔ)向量檢索(KnnVectorQuery)使用數(shù)值向量進(jìn)行近似最近鄰查詢,適用于檢索增強(qiáng)生成(RAG)、推薦系統(tǒng)、相似性檢測(cè)、自然語(yǔ)言處理與語(yǔ)義搜索等場(chǎng)景。如何使用向量檢索,請(qǐng)參見(jiàn)向量檢索介紹與使用。
向量檢索支持的距離度量算法包括歐氏距離(euclidean)、余弦相似度(cosine)、點(diǎn)積(dot_product)。不同距離度量算法的評(píng)分公式不同,表格存儲(chǔ)內(nèi)部通過(guò)距離度量算法的評(píng)分公式來(lái)評(píng)估向量之間的相似度。具體評(píng)分公式請(qǐng)參見(jiàn)下表。
MetricType | 評(píng)分公式 |
歐氏距離(euclidean) | |
點(diǎn)積(dot_product) | |
余弦相似度(cosine) |
排查分析
1. 檢查排序方式
在使用向量檢索時(shí),請(qǐng)您手動(dòng)設(shè)置排序規(guī)則為按照分?jǐn)?shù)排序,即使用ScoreSort
。默認(rèn)情況下按照主鍵排序。
2. 調(diào)整與BoolQuery的組合使用方式
如果您在組合使用KnnVectorQuery
(向量檢索)與BoolQuery
(多條件組合查詢),建議將多元索引的查詢類型設(shè)置為KnnVectorQuery
。BoolQuery
的查詢條件設(shè)置到Filter
(向量檢索過(guò)濾器)中,不影響評(píng)分分?jǐn)?shù)計(jì)算。
如果您將查詢類型設(shè)置為BoolQuery
,KnnVectorQuery
作為BoolQuery
中的子條件,則BoolQuery
中的其他查詢條件可能影響評(píng)分分?jǐn)?shù)的計(jì)算。更多信息,請(qǐng)參見(jiàn)與BoolQuery組合使用說(shuō)明。
以下為向量檢索的Java示例代碼。
private static void knnVectorQuery(SyncClient client) {
SearchQuery searchQuery = new SearchQuery();
KnnVectorQuery query = new KnnVectorQuery();
query.setFieldName("Col_Vector");
query.setTopK(10); // 返回最鄰近的topK。
query.setFloat32QueryVector(new float[]{0.1f, 0.2f, 0.3f, 0.4f});
// 最鄰近的向量需要滿足Col_Keyword=hangzhou && Col_Long<4條件。
query.setFilter(QueryBuilders.bool()
.must(QueryBuilders.term("Col_Keyword", "hangzhou"))
.must(QueryBuilders.range("Col_Long").lessThan(4))
);
searchQuery.setQuery(query);
searchQuery.setLimit(10);
// 按照分?jǐn)?shù)排序。
searchQuery.setSort(new Sort(Collections.singletonList(new ScoreSort())));
SearchRequest searchRequest = new SearchRequest("<TABLE_NAME>", "<SEARCH_INDEX_NAME>", searchQuery);
SearchRequest.ColumnsToGet columnsToGet = new SearchRequest.ColumnsToGet();
columnsToGet.setColumns(Arrays.asList("Col_Keyword", "Col_Long"));
searchRequest.setColumnsToGet(columnsToGet);
// 訪問(wèn)Search接口。
SearchResponse resp = client.search(searchRequest);
for (SearchHit hit : resp.getSearchHits()) {
// 打印分?jǐn)?shù)。
System.out.println(hit.getScore());
// 打印數(shù)據(jù)。
System.out.println(hit.getRow());
}
}
3. 檢查向量的生成效果
表格存儲(chǔ)僅對(duì)向量數(shù)據(jù)進(jìn)行相似度的計(jì)算,并不涉及向量生成的效果是否最佳的問(wèn)題。數(shù)據(jù)庫(kù)中的向量和查詢的向量均由外部Embedding模型生成寫入,因此在針對(duì)一些專業(yè)性特別強(qiáng)的場(chǎng)景,生成的向量可能效果不佳。接下來(lái)針對(duì)此問(wèn)題進(jìn)行排查。
使用外圍(不使用表格存儲(chǔ))直接計(jì)算分?jǐn)?shù)。
將查詢的向量命名為
向量a
,將希望召回的表格存儲(chǔ)表中的向量命名為向量b
。說(shuō)明您可以通過(guò)多元索引、二級(jí)索引或?qū)挶頂?shù)據(jù)讀取接口獲取
向量b
數(shù)據(jù)。根據(jù)附錄:向量檢索評(píng)分公式的演示代碼的
MetricFunction.COSINE.compare(a, b)
方法,計(jì)算出分?jǐn)?shù)a
。
使用表格存儲(chǔ)計(jì)算分?jǐn)?shù)。
使用表格存儲(chǔ)的向量檢索功能查詢
向量a
,然后查看返回結(jié)果中每行數(shù)據(jù)的分?jǐn)?shù)b
。對(duì)比分析。
如果表格存儲(chǔ)的向量檢索中未查詢到
向量b
所在的行數(shù)據(jù),則理論上返回結(jié)果中每行數(shù)據(jù)的分?jǐn)?shù)b
均高于分?jǐn)?shù)a
。此時(shí)可驗(yàn)證,Embedding模型生成效果不佳導(dǎo)致向量檢索效果不理想。由于在召回結(jié)果中僅存在高于用戶實(shí)際期望分?jǐn)?shù)的向量數(shù)據(jù),因此無(wú)法返回用戶所期望的較低分?jǐn)?shù)的向量數(shù)據(jù)。
建議方案。
該問(wèn)題一般發(fā)生在專業(yè)場(chǎng)景下,例如生物醫(yī)療中特殊的名詞在通用的Embedding模型下表現(xiàn)不佳,在專業(yè)場(chǎng)景下語(yǔ)義相近但是在模型中語(yǔ)義不相近,此時(shí)候您可考慮以下方案:
尋找專業(yè)領(lǐng)域的Embedding模型。
魔搭社區(qū)提供了大量現(xiàn)成的Embedding模型。您可以選擇政務(wù)、電商、醫(yī)療、法律、金融等專業(yè)領(lǐng)域的模型。更多信息,請(qǐng)參見(jiàn)Embedding模型列表。
通過(guò)合法途徑收集大量的專業(yè)語(yǔ)料,以此訓(xùn)練一個(gè)合適的Embedding模型。
附錄:向量檢索評(píng)分公式的演示代碼
以下通過(guò)Java代碼演示距離度量算法的評(píng)分公式。
import java.util.concurrent.ThreadLocalRandom;
public class CompareVector {
public static void main(String[] args) {
// a 是查詢的向量
float[] a = randomVector(512);
// b 是索引中期望返回的那一行向量
float[] b = randomVector(512);
// 這里選擇自己多元索引中自己設(shè)置的相似度量算法,輸出評(píng)分
System.out.println(MetricFunction.COSINE.compare(a, b));
}
public static float[] randomVector(int dim) {
float[] vec = new float[dim];
for (int i = 0; i < dim; i++) {
vec[i] = ThreadLocalRandom.current().nextFloat();
if (ThreadLocalRandom.current().nextBoolean()) {
vec[i] = -vec[i];
}
}
return l2normalize(vec, true);
}
public static float[] l2normalize(float[] v, boolean throwOnZero) {
double squareSum = 0.0f;
int dim = v.length;
for (float x : v) {
squareSum += x * x;
}
if (squareSum == 0) {
if (throwOnZero) {
throw new IllegalArgumentException("normalize a zero-length vector");
} else {
return v;
}
}
double length = Math.sqrt(squareSum);
for (int i = 0; i < dim; i++) {
v[i] /= length;
}
return v;
}
public enum MetricFunction {
/**
* Euclidean distance.
*/
EUCLIDEAN {
@Override
public float compare(float[] v1, float[] v2) {
return 1 / (1 + VectorUtil.squareDistance(v1, v2));
}
},
/**
* Dot product.
*/
DOT_PRODUCT {
@Override
public float compare(float[] v1, float[] v2) {
return (1 + VectorUtil.dotProduct(v1, v2)) / 2;
}
},
/**
* Cosine.
*/
COSINE {
@Override
public float compare(float[] v1, float[] v2) {
return (1 + VectorUtil.cosine(v1, v2)) / 2;
}
};
public abstract float compare(float[] v1, float[] v2);
}
static final class VectorUtil {
private static void checkParam(float[] a, float[] b) {
if (a.length != b.length) {
throw new IllegalArgumentException("vector dimensions differ: " + a.length + "!=" + b.length);
}
}
public static float dotProduct(float[] a, float[] b) {
checkParam(a, b);
float res = 0f;
for (int i = 0; i < a.length; i++) {
res += b[i] * a[i];
}
return res;
}
public static float cosine(float[] a, float[] b) {
checkParam(a, b);
float sum = 0.0f;
float norm1 = 0.0f;
float norm2 = 0.0f;
for (int i = 0; i < a.length; i++) {
float elem1 = a[i];
float elem2 = b[i];
sum += elem1 * elem2;
norm1 += elem1 * elem1;
norm2 += elem2 * elem2;
}
return (float) (sum / Math.sqrt((double) norm1 * (double) norm2));
}
public static float squareDistance(float[] a, float[] b) {
checkParam(a, b);
float sum = 0.0f;
for (int i = 0; i < a.length; i++) {
float difference = a[i] - b[i];
sum += difference * difference;
}
return sum;
}
}
}