語義檢索學習
1. 基礎概念篇
1.1 什麼是語義搜索?
**語義搜索是一種解讀單詞和短語含義的搜索引擎技術。**語義搜索的結果將返回與查詢含義相匹配的內容,而不是與查詢字面意思相匹配的內容。
語義搜索是一系列的搜索引擎功能,包括從搜索者的意圖及其搜索上下文中理解單詞。
這種類型的搜索旨在根據上下文更準確地解讀自然語言來提高搜索結果的質量。語義搜索藉助機器學習和人工智能等技術,將搜索意圖與語義相匹配,從而實現這一目標。
1.2 語義搜索 vs 關鍵詞搜索
關鍵詞搜索(傳統搜索):
- 基於精確匹配或模糊匹配
- 依賴詞頻統計(TF-IDF、BM25)
- 無法理解語義和上下文
- 示例:搜索"蘋果手機"只能匹配包含這些字的文檔
語義搜索:
- 理解查詢意圖和文檔含義
- 基於向量相似度
- 能夠匹配同義詞、相關概念
- 示例:搜索"蘋果手機"也能匹配"iPhone"、“iOS設備”
對比示例:
|
查詢
|
關鍵詞搜索結果
|
語義搜索結果
|
|
“如何學習編程”
|
包含"學習"和"編程"的文檔
|
編程教程、編程入門指南、代碼學習資源
|
|
“Python 性能優化”
|
包含這些關鍵詞的文檔
|
Python 性能分析、代碼加速、內存優化等相關內容
|
1.3 語義搜索的應用場景
- 智能問答系統
- 客户服務機器人
- 企業知識庫問答
- 醫療諮詢系統
- 推薦系統
- 相似文檔推薦
- 內容發現
- 個性化推薦
- 文檔檢索
- 法律文檔檢索
- 學術論文搜索
- 企業文檔管理
- 電商搜索
- 商品語義搜索
- 用户意圖理解
- 跨類目推薦
1.4 語義搜索的優勢與侷限
優勢:
- ✅ 理解用户真實意圖
- ✅ 支持自然語言查詢
- ✅ 發現相關但不完全匹配的內容
- ✅ 跨語言搜索能力
- ✅ 抗同義詞和多義詞幹擾
侷限:
- ❌ 需要高質量的向量模型
- ❌ 計算成本較高
- ❌ 對精確匹配查詢可能不如傳統搜索
- ❌ 向量模型可能存在偏見
- ❌ 可解釋性較差
2. 技術原理篇
2.1 向量化表示(Embeddings)
什麼是 Embeddings?
Embeddings(嵌入向量)是將文本、圖像等高維數據映射到低維連續向量空間的技術。相似的內容在向量空間中距離較近。
2.1.1 詞向量(Word Embeddings)
將單個詞映射為向量。
Word2Vec
- 兩種訓練方式:CBOW(連續詞袋)和 Skip-gram
- 通過上下文預測目標詞或通過目標詞預測上下文
- 向量維度通常為 100-300
示例:
king - man + woman ≈ queen
GloVe(Global Vectors)
- 基於全局詞共現統計
- 結合了全局矩陣分解和局部上下文窗口方法
FastText
- Facebook 開發
- 考慮子詞信息(subword)
- 對拼寫錯誤和罕見詞更魯棒
2.1.2 句子向量(Sentence Embeddings)
將整個句子映射為單個向量。
Sentence-BERT (SBERT)
- 基於 BERT 的孿生網絡架構
- 專門優化用於語義相似度任務
- 效率高,適合大規模檢索
Universal Sentence Encoder (USE)
- Google 開發
- 支持多語言
- 兩種模型:Transformer 和 DAN(Deep Averaging Network)
示例代碼(使用 gte-multilingual-base):
package com.zhouquan.ai;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.util.*;
/**
* GTE 向量生成示例
* <p>
* 使用 gte-multilingual-base 模型批量生成文本向量
* </p>
*
* @author zhouquan
*/
public class SentenceEmbeddingExample {
// GTE 服務配置
private static final String EMBEDDING_SERVICE_URL = "http://192.168.0.168:5000";
private static final int VECTOR_DIMENSION = 768;
public static void main(String[] args) {
try {
System.out.println("GTE 向量生成示例");
System.out.println("模型: Alibaba-NLP/gte-multilingual-base");
System.out.println("服務地址: " + EMBEDDING_SERVICE_URL);
System.out.println("向量維度: " + VECTOR_DIMENSION);
System.out.println();
// 測試句子
List<String> sentences = Arrays.asList(
"如何學習 Java 編程",
"Java 入門教程",
"今天天氣真好"
);
// 批量文本向量化
System.out.println("【批量文本向量化】");
System.out.println();
List<float[]> embeddings = embedBatchTexts(sentences);
for (int i = 0; i < sentences.size(); i++) {
System.out.println("句子: " + sentences.get(i));
// 向量只打印前五個
System.out.print("向量: ");
for (int j = 0; j < Math.min(5, embeddings.get(i).length); j++) {
System.out.print(embeddings.get(i)[j] + " ");
}
System.out.println("向量維度: " + embeddings.get(i).length);
System.out.println();
}
} catch (Exception e) {
System.err.println("錯誤: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 批量文本向量化
*
* @param texts 待向量化的文本列表
* @return 向量列表
*/
public static List<float[]> embedBatchTexts(List<String> texts) {
try {
RestTemplate restTemplate = new RestTemplate();
String url = EMBEDDING_SERVICE_URL + "/embed_batch";
// 構建請求
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("texts", texts);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
// 發送請求
ResponseEntity<Map> response = restTemplate.postForEntity(url, request, Map.class);
// 解析響應
Map<String, Object> body = response.getBody();
if (body == null || !body.containsKey("embeddings")) {
throw new RuntimeException("響應數據為空");
}
List<List<Double>> embeddings = (List<List<Double>>) body.get("embeddings");
List<float[]> result = new ArrayList<>();
for (List<Double> embedding : embeddings) {
result.add(convertToFloatArray(embedding));
}
return result;
} catch (Exception e) {
throw new RuntimeException("批量文本向量化失敗: " + e.getMessage(), e);
}
}
/**
* 將 List<Double> 轉換為 float[]
*/
private static float[] convertToFloatArray(List<Double> list) {
float[] array = new float[list.size()];
for (int i = 0; i < list.size(); i++) {
array[i] = list.get(i).floatValue();
}
return array;
}
}
輸出結果:
GTE 向量生成示例
模型: Alibaba-NLP/gte-multilingual-base
服務地址: http://192.168.0.168:5000
向量維度: 768
【批量文本向量化】
句子: 如何學習 Java 編程
向量: 0.026278736 -0.027642524 -0.11199176 -0.052430928 0.050870888 向量維度: 768
句子: Java 入門教程
向量: -0.014897345 0.011146749 -0.091105774 -0.050095912 0.012215903 向量維度: 768
句子: 今天天氣真好
向量: -0.030632775 0.057086878 -0.06665294 -0.026957892 -0.02842947 向量維度: 768
2.1.3 文檔向量(Document Embeddings)
將長文檔映射為向量。
Doc2Vec
- Word2Vec 的擴展
- 在訓練時加入文檔 ID
長文本向量化策略:
- 分塊平均:將文檔分塊,對每塊向量取平均
- 加權平均:使用 TF-IDF 等權重
- 層次化編碼:先編碼段落,再聚合為文檔向量
- 長文本模型:如 Longformer、BigBird
2.1.4 常用向量化模型對比
|
模型
|
類型
|
向量維度
|
多語言
|
適用場景
|
|
Word2Vec
|
詞向量
|
100-300
|
❌
|
詞語相似度
|
|
GloVe
|
詞向量
|
50-300
|
❌
|
詞語相似度
|
|
BERT
|
上下文詞向量
|
768/1024
|
✅
|
NLP 任務
|
|
Sentence-BERT
|
句子向量
|
384/768
|
✅
|
句子相似度、檢索
|
|
USE
|
句子向量
|
512
|
✅
|
句子相似度、分類
|
|
OpenAI text-embedding-ada-002
|
文本向量
|
1536
|
✅
|
通用文本檢索
|
|
text-embedding-3-small
|
文本向量
|
1536
|
✅
|
高效檢索
|
|
text-embedding-3-large
|
文本向量
|
3072
|
✅
|
高精度檢索
|
2.2 向量相似度計算
2.2.1 餘弦相似度(Cosine Similarity)
向量相似度計算可視化
最常用的相似度度量方法,計算兩個向量之間的夾角。
2D 向量空間可視化
**説明:**實際的 GTE 模型生成的是 768 維向量,為了便於理解,這裏先用 2D 向量展示餘弦相似度的幾何意義。
3D 向量空間可視化
公式:
公式:
cosine_similarity(A, B) = (A · B) / (||A|| × ||B||)
其中:
• A · B = 點積 = A₀×B₀ + A₁×B₁ + A₂×B₂ + ... + A₇₆₇×B₇₆₇
• ||A|| = 向量A的模長 = √(A₀² + A₁² + A₂² + ... + A₇₆₇²)
• ||B|| = 向量B的模長 = √(B₀² + B₁² + B₂² + ... + B₇₆₇²)
特點:
- 值域:[-1, 1],1 表示完全相同,-1 表示完全相反
- 不受向量長度影響,只關注方向
- 最適合文本向量相似度
Java 實現:
/**
* 計算餘弦相似度
*
* @param vec1 向量1
* @param vec2 向量2
* @return 相似度值 (0-1之間,越大越相似)
*/
public static double cosineSimilarity(float[] vec1, float[] vec2) {
// 初始化三個累加器
double dotProduct = 0.0; // 點積:A·B
double norm1 = 0.0; // ||A||²
double norm2 = 0.0; // ||B||²
// 遍歷所有維度(768維)
for (int i = 0; i < vec1.length; i++) {
dotProduct += vec1[i] * vec2[i]; // 累加點積
norm1 += vec1[i] * vec1[i]; // 累加A的平方和
norm2 += vec2[i] * vec2[i]; // 累加B的平方和
}
// 返回餘弦相似度
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}
詳細計算過程(前5個維度)
2.2.2 歐氏距離(Euclidean Distance)
歐氏距離可視化詳解
計算向量在空間中的直線距離。
2D 空間直觀理解
公式:
euclidean_distance(A, B) = sqrt(Σ(Ai - Bi)²)
特點:
- 值域:[0, ∞),0 表示完全相同
- 受向量長度影響
- 適合歸一化後的向量
768 維向量的歐氏距離計算
Java 實現:
/**
* 計算歐氏距離
*
* @param vec1 向量1 (768維)
* @param vec2 向量2 (768維)
* @return 歐氏距離值 (範圍: [0, ∞), 0表示完全相同)
*/
public static double euclideanDistance(float[] vec1, float[] vec2) {
if (vec1.length != vec2.length) {
throw new IllegalArgumentException("向量維度不匹配");
}
double sumSquaredDiff = 0.0; // 差值平方和
// 遍歷所有維度(768維)
for (int i = 0; i < vec1.length; i++) {
double diff = vec1[i] - vec2[i]; // 計算差值
sumSquaredDiff += diff * diff; // 累加差值平方
}
// 返回平方根
return Math.sqrt(sumSquaredDiff);
}
/**
* 歸一化歐氏距離(範圍轉換為 [0, 1])
*
* @param vec1 向量1
* @param vec2 向量2
* @return 歸一化後的距離 (0表示相同, 1表示最遠)
*/
public static double normalizedEuclideanDistance(float[] vec1, float[] vec2) {
double distance = euclideanDistance(vec1, vec2);
// 對於單位向量,最大距離是 √2
double maxDistance = Math.sqrt(2.0);
return distance / maxDistance;
}
/**
* 將歐氏距離轉換為相似度 (範圍: [0, 1])
*
* @param vec1 向量1
* @param vec2 向量2
* @return 相似度 (1表示相同, 0表示最遠)
*/
public static double euclideanSimilarity(float[] vec1, float[] vec2) {
double distance = euclideanDistance(vec1, vec2);
// 使用公式: similarity = 1 / (1 + distance)
return 1.0 / (1.0 + distance);
}
2.2.3 點積相似度(Dot Product)
點積相似度可視化詳解
計算兩個向量的點積。
公式:
代數定義:
dot_product(A, B) = Σ(Aᵢ × Bᵢ)
展開形式:
A · B = A₀×B₀ + A₁×B₁ + A₂×B₂ + ... + A₇₆₇×B₇₆₇
幾何定義:
A · B = ||A|| × ||B|| × cos(θ)
其中:
• ||A|| = 向量 A 的長度(模長)
• ||B|| = 向量 B 的長度(模長)
• θ = 向量 A 和 B 之間的夾角
特點:
- 值域:(-∞, ∞)
- 同時考慮方向和長度
- 計算效率高
2D 空間幾何理解
Java 實現:
/**
* 計算點積(內積)
*
* @param vec1 向量1 (768維)
* @param vec2 向量2 (768維)
* @return 點積值 (範圍: (-∞, +∞))
*/
public static double dotProduct(float[] vec1, float[] vec2) {
if (vec1.length != vec2.length) {
throw new IllegalArgumentException("向量維度不匹配");
}
double result = 0.0;
// 遍歷所有維度(768維)- 最簡單高效的計算
for (int i = 0; i < vec1.length; i++) {
result += vec1[i] * vec2[i]; // 只需乘法和加法
}
return result;
}
/**
* 點積與餘弦相似度的關係
* cosine = dotProduct / (||A|| × ||B||)
*/
public static double cosineSimilarityFromDotProduct(float[] vec1, float[] vec2) {
double dot = dotProduct(vec1, vec2);
double norm1 = Math.sqrt(dotProduct(vec1, vec1)); // ||A|| = √(A·A)
double norm2 = Math.sqrt(dotProduct(vec2, vec2)); // ||B|| = √(B·B)
return dot / (norm1 * norm2);
}
/**
* 對於歸一化向量,點積就等於餘弦相似度
* 因為 ||A|| = ||B|| = 1
*/
public static double dotProductNormalized(float[] vec1, float[] vec2) {
// 假設向量已經歸一化
// 此時 dot(A, B) = ||A|| × ||B|| × cos(θ) = 1 × 1 × cos(θ) = cos(θ)
return dotProduct(vec1, vec2);
}
/**
* 使用 SIMD 優化的點積(性能優化版本)
* 實際生產中可以使用 Java Vector API (JDK 16+)
*/
public static double dotProductOptimized(float[] vec1, float[] vec2) {
double sum0 = 0, sum1 = 0, sum2 = 0, sum3 = 0; // 4路並行累加
int i = 0;
int limit = vec1.length - 3;
// 循環展開,利用 CPU 流水線
for (; i < limit; i += 4) {
sum0 += vec1[i] * vec2[i];
sum1 += vec1[i+1] * vec2[i+1];
sum2 += vec1[i+2] * vec2[i+2];
sum3 += vec1[i+3] * vec2[i+3];
}
// 處理剩餘元素
for (; i < vec1.length; i++) {
sum0 += vec1[i] * vec2[i];
}
return sum0 + sum1 + sum2 + sum3;
}
2.2.4 相似度度量選擇建議
|
場景
|
推薦度量
|
原因
|
|
文本語義檢索
|
餘弦相似度
|
不受文本長度影響
|
|
歸一化向量
|
點積或餘弦
|
計算效率高
|
|
圖像檢索
|
L2 距離或餘弦
|
根據模型訓練方式選擇
|
|
推薦系統
|
餘弦相似度
|
關注內容相關性
|
2.3 向量搜索技術
2.3.1 HNSW(Hierarchical Navigable Small World)
原理: 分層圖結構 + 貪婪搜索
- 構建多層圖,每層節點數遞減(類似跳錶)
- 上層用於快速跳躍,下層用於精確搜索
- 搜索時從頂層開始,逐層下降找到最近鄰
- 時間複雜度: O(log N) 查詢
- 空間複雜度: O(N × M),M 為連接數
特點:
- ✅ 查詢速度快,適合實時檢索
- ✅ 召回率高(通常 > 95%)
- ✅ 工業界廣泛應用,技術成熟
- ❌ 內存佔用大
- ❌ 構建索引較慢
搜索步驟詳解
1 進入頂層(高速路):從入口點出發,在稀疏的頂層節點間跳躍,快速接近目標區域
2下降到中層(省道):在中層繼續搜索,節點更密集,進一步縮小範圍
3 到達底層(街道):在最密集的底層進行精確搜索,找到最近鄰
應用場景:
- Elasticsearch(默認使用 HNSW)
- Faiss(Facebook AI 向量搜索庫)
- Milvus(開源向量數據庫)
- Pinecone、Weaviate 等向量數據庫
推薦指數:⭐⭐⭐⭐⭐
適合 95% 的向量搜索場景,工業界首選方案。
2.3.2 IVF(Inverted File Index)
原理:
- 使用聚類(如 K-Means)將向量空間劃分為多個區域
- 搜索時只在最近的幾個聚類中查找
- 類似倒排索引的思想
特點:
- ✅ 內存佔用小
- ✅ 適合超大規模數據(億級以上)
- ✅ 可與 PQ(Product Quantization)結合進一步壓縮
- ❌ 召回率相對較低(80-95%)
- ❌ 需要選擇合適的聚類數
搜索步驟詳解
聚類階段(預處理):使用 K-Means 將所有向量分成 N 個簇,計算每個簇的中心點
找到最近的簇:計算查詢向量到所有聚類中心的距離,找出最近的 nprobe 個簇
只在選中的簇內搜索:只計算這 nprobe 個簇內數據點的距離,跳過其他簇
應用場景:
- Faiss IVF 系列索引
- 超大規模向量檢索(> 千萬級)
- 對內存有嚴格限制的場景
推薦指數:⭐⭐⭐⭐
超大規模數據的首選,需要在召回率和性能間權衡。
2.3.3 暴力搜索(Brute Force)
原理:
- 計算查詢向量與所有候選向量的相似度
- 返回 Top-K 最相似的結果
特點:
- ✅ 精確搜索,召回率 100%
- ✅ 實現簡單,無需調參
- ✅ 適合小規模數據驗證
- ❌ 時間複雜度 O(N×D),N 為文檔數,D 為向量維度
- ❌ 不適合大規模數據
應用場景:
- 小規模數據(< 10萬)
- 驗證其他算法的召回率基準
- 原型開發和測試
推薦指數:⭐⭐
僅用於小規模場景或作為對比基準。
2.3.4 LSH(Locality-Sensitive Hashing)
原理:
- 使用哈希函數將相似向量映射到相同的桶
- 只在相同桶內搜索
特點:
- ✅ 構建速度快
- ✅ 內存友好
- ✅ 適合動態更新場景
- ❌ 召回率較低(70-90%)
- ❌ 參數調優困難
- ❌ 對高維向量效果不佳
應用場景:
- 對精度要求不高的推薦系統
- 需要頻繁更新索引的場景
- 低維向量檢索
推薦指數:⭐⭐
現代場景中已較少使用,多被 HNSW 替代。
2.3.5 向量索引算法對比
總體推薦
|
算法
|
流行度
|
查詢速度
|
召回率
|
內存佔用
|
構建速度
|
適用場景
|
推薦指數
|
|
HNSW |
⭐⭐⭐⭐⭐
|
快
|
95-99%
|
高
|
中等
|
中大規模實時檢索
|
⭐⭐⭐⭐⭐
|
|
IVF |
⭐⭐⭐⭐
|
中等
|
80-95%
|
中等
|
快
|
超大規模數據(億級)
|
⭐⭐⭐⭐
|
|
Brute Force
|
⭐⭐
|
慢
|
100%
|
低
|
快
|
小規模數據(< 10萬)
|
⭐⭐
|
|
LSH
|
⭐
|
快
|
70-90%
|
低
|
快
|
對精度要求不高的場景
|
⭐⭐
|
查詢速度對比
召回率對比
選擇建議:
- 首選 HNSW:適合 95% 的場景,Elasticsearch 默認方案
- 超大規模用 IVF:數據量 > 千萬級,內存受限
- 小規模用暴力:< 10萬數據,追求 100% 召回
- 避免使用 LSH**:已有更好的替代方案
2.3.6 向量搜索性能優化
- 向量維度優化
- 降維技術:PCA、t-SNE
- 使用較小的模型:如 MiniLM 系列
- 量化技術
- 標量量化:float32 → int8
- 乘積量化(PQ):將向量分段量化
- 硬件加速
- GPU 加速
- 專用向量搜索硬件
3. Elasticsearch 實踐篇
3.1 Elasticsearch 中的向量搜索
Elasticsearch 從 7.x 版本開始支持向量搜索,8.x 版本功能更加完善。
3.1.1 dense_vector 字段類型
字段定義:
PUT /my_index
{
"mappings": {
"properties": {
"title": {
"type": "text"
},
"content": {
"type": "text"
},
"title_vector": {
"type": "dense_vector",
"dims": 768,
"index": true,
"similarity": "cosine"
},
"content_vector": {
"type": "dense_vector",
"dims": 768,
"index": true,
"similarity": "cosine"
}
}
}
}
參數説明:
dims:向量維度,必須與模型輸出維度一致index:是否創建向量索引(true = HNSW 索引)similarity:相似度度量方式
cosine:餘弦相似度(推薦)dot_product:點積l2_norm:歐氏距離
索引數據
測試數據:zhouxiaoquan.github.io/zhouquan.github.io/esdata.txt
POST /my_index/_doc
{
"title": "Python 數據分析實戰",
"content": "本課程將介紹如何使用 Python 進行數據分析,包括 NumPy、Pandas、Matplotlib 等庫的使用,從數據清洗到可視化,帶你掌握數據分析的核心技能。",
"title_vector": [ -0.0208999402821064,
0.01331911701709032,
-0.06355377286672592
...
],
"content_vector": [-0.03954795375466347,
0.007010910660028458,
-0.07348231226205826,
...
]
}
POST /my_index/_doc
{
"title": "機器學習基礎",
"content": "機器學習是人工智能的一個分支,它使計算機能夠學習而不需要明確編程。本課程將從監督學習、無監督學習等基礎概念講起,帶你進入機器學習的世界。",
"title_vector": [ -0.013987698592245579,
-0.0579521618783474,
-0.08713562786579132
],
"content_vector": [ -0.030959052965044975,
-0.042195241898298264,
-0.0682775005698204,
...
]
}
POST /my_index/_doc
{
"title": "Elasticsearch 分佈式搜索",
"content": "Elasticsearch 是一個基於 Lucene 的分佈式搜索引擎,它提供了一個分佈式多用户能力的全文搜索引擎,具有高可用性、高擴展性和實時性。",
"title_vector": [ -0.08578959107398987,
0.02684123069047928,
-0.03178650140762329,
...
],
"content_vector": [
-0.09641386568546295,
0.017185380682349205,
-0.004696432966738939,
...
]
}
3.1.2 kNN 搜索
基本 kNN 查詢:
POST /my_index/_search
{
"_source": ["title", "content"],
"knn": {
"field": "title_vector",
"query_vector": [ // machine learning
0.008781400509178638,
-0.050233472138643265,
-0.050182443112134933
...
],
"k": 2,
"num_candidates": 4
}
}
參數説明:
field:向量字段名query_vector:查詢向量k:返回 Top-K 結果num_candidates:候選數量,越大召回率越高但速度越慢(建議 >= 2k)
檢索結果:檢索詞machine learning,得分相關度最高為機器學習基礎
帶過濾條件的 kNN 查詢:
POST /my_index/_search
{
"knn": {
"field": "content_vector",
"query_vector": [0.1, 0.2, 0.3, ...],
"k": 10,
"num_candidates": 100,
"filter": {
"term": {
"category": "技術文檔"
}
}
}
}
3.1.3 向量維度選擇
常見維度及其特點:
|
維度
|
存儲空間
|
搜索速度
|
精度
|
適用場景
|
|
128
|
小
|
快
|
中等
|
對精度要求不高的場景
|
|
384
|
中等
|
中等
|
高
|
通用推薦(SBERT mini)
|
|
768
|
大
|
慢
|
很高
|
高精度要求
|
|
1536
|
很大
|
很慢
|
非常高
|
OpenAI embeddings
|
選擇建議:
- 數據量 < 100萬:可選擇 768 或更高維度
- 數據量 100萬 - 1000萬:建議 384 維度
- 數據量 > 1000萬:建議 256 或 384 維度,考慮降維
3.2 文本向量化方案
3.2.1 集成外部向量化服務
架構模式:
應用程序 → 向量化服務 → Elasticsearch
↓
[批量處理隊列]
使用 Spring Boot 構建向量化服務:
/**
* 批量將文本轉換為向量
* <p>
* 支持自動重試機制,最多重試3次,採用指數退避策略。
* </p>
*
* @param texts 待向量化的文本列表
* @return 向量列表
*/
public List<float[]> embedTexts(List<String> texts) {
if (texts == null || texts.isEmpty()) {
return new ArrayList<>();
}
int maxRetries = 3;
int retryDelay = 1000; // 初始延遲 1秒
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
String url = appConfig.getEmbeddingServiceUrl() + "/embed_batch";
log.info("[向量化] 調用本地 embedding 服務: {}, 文本數量: {}, 嘗試次數: {}/{}",
url, texts.size(), attempt, maxRetries);
// 計算請求大小
long totalChars = texts.stream().mapToLong(String::length).sum();
log.debug("[向量化] 請求文本總字符數: {}", totalChars);
// 構建請求體
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("texts", texts);
// 設置請求頭
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Connection", "keep-alive");
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
// 發送 POST 請求
ResponseEntity<EmbedBatchResponse> response = restTemplate.postForEntity(
url,
request,
EmbedBatchResponse.class
);
EmbedBatchResponse body = response.getBody();
if (body == null || body.getEmbeddings() == null || body.getEmbeddings().isEmpty()) {
log.error("[向量化] 向量化服務返回結果為空");
throw new RuntimeException("向量化服務返回結果為空");
}
log.info("[向量化] 成功,返回 {} 個向量,維度: {}", body.getCount(), body.getDimension());
// 將 List<List<Double>> 轉換為 List<float[]>
List<float[]> result = new ArrayList<>();
for (List<Double> embedding : body.getEmbeddings()) {
float[] floatArray = new float[embedding.size()];
for (int i = 0; i < embedding.size(); i++) {
floatArray[i] = embedding.get(i).floatValue();
}
result.add(floatArray);
}
return result;
} catch (ResourceAccessException e) {
if (attempt == maxRetries) {
log.error("[向量化] 失敗,已達最大重試次數 {}", maxRetries, e);
throw new RuntimeException("批量文本向量化失敗(連接錯誤): " + e.getMessage() +
"。請檢查向量化服務是否正常運行: " + appConfig.getEmbeddingServiceUrl(), e);
}
log.warn("[向量化] 第 {} 次嘗試失敗(連接錯誤),{}ms 後重試: {}",
attempt, retryDelay, e.getMessage());
try {
Thread.sleep(retryDelay);
retryDelay *= 2; // 指數退避
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重試被中斷", ie);
}
} catch (Exception e) {
log.error("[向量化] 失敗,文本數量: {}", texts.size(), e);
throw new RuntimeException("批量文本向量化失敗: " + e.getMessage(), e);
}
}
throw new RuntimeException("批量文本向量化失敗:未知錯誤");
}
3.2.2 Elasticsearch Inference API
Elasticsearch 8.8+ 支持內置的推理 API,可以直接調用機器學習模型。
配置推理模型:
PUT _ml/trained_models/sentence-transformers
{
"input": {
"field_names": ["text"]
},
"inference_config": {
"text_embedding": {}
}
}
使用推理處理器自動向量化:
PUT _ingest/pipeline/text-embeddings
{
"description": "Text embedding pipeline",
"processors": [
{
"inference": {
"model_id": "sentence-transformers",
"target_field": "text_embedding",
"field_map": {
"content": "text"
}
}
}
]
}
索引時自動生成向量:
POST /my_index/_doc?pipeline=text-embeddings
{
"content": "這是一段需要向量化的文本"
}
3.3 混合搜索策略
混合搜索結合了語義搜索和傳統關鍵詞搜索的優勢。
3.3.1 語義搜索 + 關鍵詞搜索
基本混合查詢:
POST /my_index/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"content": {
"query": "Elasticsearch 教程",
"boost": 1.0
}
}
}
]
}
},
"knn": {
"field": "content_vector",
"query_vector": [0.1, 0.2, ...],
"k": 10,
"num_candidates": 100,
"boost": 2.0
}
}
參數説明:
boost:調整語義搜索和關鍵詞搜索的權重比例- 語義搜索 boost 越大,越重視語義相關性
- 關鍵詞搜索 boost 越大,越重視精確匹配
3.3.3 混合搜索最佳實踐
場景一:精確匹配優先
{
"query": {
"match": {
"title": {
"query": "iPhone 15 Pro",
"boost": 3.0
}
}
},
"knn": {
"field": "description_vector",
"query_vector": [...],
"k": 10,
"num_candidates": 100,
"boost": 1.0
}
}
場景二:語義理解優先
{
"query": {
"match": {
"content": {
"query": "如何提升代碼質量",
"boost": 0.5
}
}
},
"knn": {
"field": "content_vector",
"query_vector": [...],
"k": 10,
"num_candidates": 100,
"boost": 3.0
}
}
參考資源
官方文檔
- Elasticsearch 官方文檔
- Dense vector field type
- kNN search
- Semantic search tutorial
- Sentence-BERT
- 官方網站
- 模型庫
- GitHub
- OpenAI Embeddings
- Embeddings Guide
- text-embedding-3 模型
技術博客與教程
- 什麼是語義搜索?| Elastic
- 混合搜索最佳實踐
- LangChain RAG Tutorial
- HNSW 算法詳解
- 向量數據庫對比
- ANN Benchmarks
- Hugging Face Transformers
- Sentence Transformers
r.html)
- kNN search
- Semantic search tutorial
- Sentence-BERT
- 官方網站
- 模型庫
- GitHub
- OpenAI Embeddings
- Embeddings Guide
- text-embedding-3 模型
技術博客與教程
- 什麼是語義搜索?| Elastic
- 混合搜索最佳實踐
- LangChain RAG Tutorial
- HNSW 算法詳解
- 向量數據庫對比
- ANN Benchmarks
- Hugging Face Transformers
- Sentence Transformers