並發執行向量語義檢索
當需要同時檢索多條查詢(如批量問答、RAG 多路召回、多使用者並發搜尋),逐條串列執行會導致整體耗時隨查詢數線性增長。通過並發執行多條檢索請求,可以將總耗時從 N × 單次延遲 降低到接近 1 × 單次延遲,顯著提升輸送量。
本文介紹兩種並發方式:CLI 並發和 SDK 並發,適用於以下情境:
-
批量語義搜尋:一次性提交多條查詢文本,快速擷取全部檢索結果。
-
RAG 多路召回:為同一使用者請求同時發起多條不同角度的檢索,降低端到端延遲。
-
多模態批量檢索:同時檢索文本、圖片、視頻等不同模態的向量資料。
選擇並發方式
|
方式 |
適用情境 |
特點 |
|
CLI 並發 |
營運指令碼、一次性批量檢索、無需編寫代碼的快速驗證 |
輸入文本自動 Embedding;無需管理向量維度;Shell 指令碼即可實現 |
|
SDK 並發 |
商務服務整合、需要精細控制(過濾條件、結果後處理)、高效能後端 |
直接調用 API,可設定過濾條件;複用用戶端串連;支援 Python 和 Go |
如何選擇:
-
如果你有一批查詢文本,希望快速拿到結果而不想寫代碼 → 使用 CLI 並發。
-
如果你在開發商務服務,需要將向量檢索嵌入到應用邏輯中 → 使用 SDK 並發。
說明:CLI 方式內建了 Embedding 模型調用,輸入文本即可檢索。SDK 方式需要傳入已產生的向量,適合已有 Embedding 流程的情境。
通過 CLI 並發檢索
CLI 並發通過啟動多個 oss-vectors-embed query 進程實現並行檢索。以下提供三種實現方式,按複雜度遞增排列。
開始前,請確保滿足以下條件:
-
已安裝 OSS Vectors Embed CLI。安裝方式請參見使用OSS Vectors Embed CLI工具寫入和檢索向量資料。
-
已配置環境變數
OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET和DASHSCOPE_API_KEY。 -
已建立向量 Bucket 和向量索引,且索引維度與所用 Embedding 模型輸出維度一致。
將以下樣本中的預留位置替換為實際值:
|
預留位置 |
說明 |
|
|
阿里雲帳號 ID |
|
|
向量 Bucket 名稱 |
|
|
向量索引名稱 |
xargs 快速並發
如果不需要複雜的流程式控制制,xargs -P 是最簡單的 CLI 並發方式,一行命令即可完成。
cat queries.txt | xargs -P 5 -I {} \
oss-vectors-embed \
--account-id "<your-account-id>" \
--vectors-region cn-hangzhou \
query \
--vector-bucket-name "<your-vector-bucket>" \
--index-name "<your-index>" \
--model-id text-embedding-v4 \
--text-value "{}" \
--top-k 10
-P 5 表示最多 5 個進程並存執行。檢索結果直接輸出到終端,適合快速驗證。如需儲存結果,可將輸出重新導向到檔案。
Shell 後台並發
通過 & 將多個查詢命令放入後台並存執行,wait 等待全部完成。適合查詢數量較少(10 條以內)的情境。
#!/bin/bash
ACCOUNT_ID="<your-account-id>"
REGION="cn-hangzhou"
BUCKET="<your-vector-bucket>"
INDEX="<your-index>"
MODEL="text-embedding-v4"
queries=(
"如何配置生命週期規則"
"Object Storage Service有哪些儲存類型"
"如何設定跨地區複製"
)
mkdir -p ./query-results
# 並發啟動所有查詢,每條結果寫入獨立檔案
for i in "${!queries[@]}"; do
oss-vectors-embed \
--account-id "$ACCOUNT_ID" \
--vectors-region "$REGION" \
query \
--vector-bucket-name "$BUCKET" \
--index-name "$INDEX" \
--model-id "$MODEL" \
--text-value "${queries[$i]}" \
--top-k 10 \
--return-metadata \
> "./query-results/result_${i}.json" 2>&1 &
done
wait
echo "全部查詢完成,結果儲存在 ./query-results/"
運行後輸出:
全部查詢完成,結果儲存在 ./query-results/
每個結果檔案包含 JSON 格式的檢索結果,可通過 cat ./query-results/result_0.json | python3 -m json.tool 查看。
控制並發數的 Shell 指令碼
當查詢數量較多時(數十條以上),需要限制同時啟動並執行進程數,避免超出 API 配額。以下指令碼從檔案逐行讀取查詢文本,控制最多 5 個進程同時執行。
#!/bin/bash
ACCOUNT_ID="<your-account-id>"
REGION="cn-hangzhou"
BUCKET="<your-vector-bucket>"
INDEX="<your-index>"
MODEL="text-embedding-v4"
MAX_CONCURRENT=5
QUERY_FILE="./queries.txt" # 每行一條查詢文本
mkdir -p ./query-results
run_query() {
local idx=$1
local text=$2
oss-vectors-embed \
--account-id "$ACCOUNT_ID" \
--vectors-region "$REGION" \
query \
--vector-bucket-name "$BUCKET" \
--index-name "$INDEX" \
--model-id "$MODEL" \
--text-value "$text" \
--top-k 10 \
> "./query-results/result_${idx}.json" 2>&1
}
idx=0
while IFS= read -r query_text; do
run_query "$idx" "$query_text" &
idx=$((idx + 1))
# 達到並發上限時等待一個任務完成再繼續
if (( $(jobs -rp | wc -l) >= MAX_CONCURRENT )); then
wait -n
fi
done < "$QUERY_FILE"
wait
echo "全部 $idx 條查詢完成"
運行前準備 queries.txt 檔案,每行一條查詢文本:
如何配置生命週期規則
Object Storage Service有哪些儲存類型
如何設定跨地區複製
Bucket 標籤的使用限制
如何啟用版本控制
運行後輸出:
全部 5 條查詢完成
Python 封裝 CLI 並發
如果需要對 CLI 返回的結果進行後處理(如解析 JSON、匯總統計),可以用 Python asyncio 封裝 CLI 調用。
import asyncio
import json
from pathlib import Path
ACCOUNT_ID = "<your-account-id>"
REGION = "cn-hangzhou"
BUCKET = "<your-vector-bucket>"
INDEX = "<your-index>"
MODEL = "text-embedding-v4"
MAX_CONCURRENT = 5
async def run_query(semaphore: asyncio.Semaphore, query_text: str, query_id: int):
"""非同步執行單條 CLI query 命令"""
async with semaphore:
cmd = [
"oss-vectors-embed",
"--account-id", ACCOUNT_ID,
"--vectors-region", REGION,
"query",
"--vector-bucket-name", BUCKET,
"--index-name", INDEX,
"--model-id", MODEL,
"--text-value", query_text,
"--top-k", "10",
"--return-metadata",
]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode == 0:
result = json.loads(stdout.decode())
print(f"查詢 {query_id} 完成,返回 {len(result.get('results', [ ]))} 條結果")
return {"query_id": query_id, "query_text": query_text, "result": result}
else:
print(f"查詢 {query_id} 失敗: {stderr.decode()}")
return {"query_id": query_id, "query_text": query_text, "error": stderr.decode()}
async def batch_query(queries: list[str]):
"""批量並發執行多條查詢"""
semaphore = asyncio.Semaphore(MAX_CONCURRENT)
tasks = [
run_query(semaphore, text, idx)
for idx, text in enumerate(queries)
]
results = await asyncio.gather(*tasks)
output_path = Path("./query-results/batch_results.json")
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(results, ensure_ascii=False, indent=2))
print(f"匯總結果已儲存到 {output_path}")
return results
if __name__ == "__main__":
queries = [
"如何配置生命週期規則",
"Object Storage Service有哪些儲存類型",
"如何設定跨地區複製",
"Bucket 標籤的使用限制",
"如何啟用版本控制",
]
asyncio.run(batch_query(queries))
運行後輸出:
查詢 0 完成,返回 10 條結果
查詢 1 完成,返回 10 條結果
查詢 2 完成,返回 10 條結果
查詢 3 完成,返回 10 條結果
查詢 4 完成,返回 10 條結果
匯總結果已儲存到 query-results/batch_results.json
通過 SDK 並發檢索
SDK 並發直接調用 query_vectors API,無需啟動外部進程。適合需要精細控制檢索參數(如設定過濾條件)或將檢索整合到商務服務中的情境。
說明:SDK 方式需要傳入已產生的查詢向量(如 float32 數組),而非原始文本。如果你的情境是從文本出發檢索,建議先通過 Embedding 模型產生向量,或直接使用上文的 CLI 並發方式。
Python SDK 並發檢索
使用 alibabacloud-oss-v2 Python SDK 通過線程池並發調用 query_vectors 介面。開始前請安裝 SDK:
pip install alibabacloud-oss-v2
確保已配置環境變數 OSS_ACCESS_KEY_ID 和 OSS_ACCESS_KEY_SECRET,並已建立向量 Bucket 和索引。
基本樣本
以下樣本並發檢索 5 組向量,使用 ThreadPoolExecutor 控制最多 5 個線程同時執行:
import json
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
import alibabacloud_oss_v2 as oss
import alibabacloud_oss_v2.vectors as oss_vectors
ACCOUNT_ID = "<your-account-id>"
REGION = "cn-hangzhou"
BUCKET = "<your-vector-bucket>"
INDEX = "<your-index>"
MAX_CONCURRENT = 5
def create_vector_client():
"""建立向量檢索用戶端(全域複用,避免重複建立串連)"""
credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider()
cfg = oss.config.load_default()
cfg.credentials_provider = credentials_provider
cfg.region = REGION
cfg.account_id = ACCOUNT_ID
cfg.use_internal_endpoint = True # 如需通過公網訪問,請將此處設定為False或刪除此行
return oss_vectors.Client(cfg)
def run_query(client, query_vector, query_id, query_filter=None):
"""執行單條向量檢索"""
request = oss_vectors.models.QueryVectorsRequest(
bucket=BUCKET,
index_name=INDEX,
query_vector=query_vector,
filter=query_filter,
return_distance=True,
return_metadata=True,
top_k=10,
)
result = client.query_vectors(request)
print(f"查詢 {query_id} 完成,status code: {result.status_code}")
return {
"query_id": query_id,
"status_code": result.status_code,
"vectors": [str(v) for v in result.vectors] if result.vectors else [ ],
}
def batch_query(query_vectors):
"""批量並發執行多條向量檢索"""
client = create_vector_client()
results = [ ]
with ThreadPoolExecutor(max_workers=MAX_CONCURRENT) as executor:
futures = {
executor.submit(run_query, client, qv, idx): idx
for idx, qv in enumerate(query_vectors)
}
for future in as_completed(futures):
idx = futures[future]
try:
results.append(future.result())
except Exception as e:
print(f"查詢 {idx} 失敗: {e}")
results.append({"query_id": idx, "error": str(e)})
results.sort(key=lambda x: x["query_id"])
output_path = Path("./query-results/sdk_batch_results.json")
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(results, ensure_ascii=False, indent=2))
print(f"匯總結果已儲存到 {output_path}")
return results
if __name__ == "__main__":
# 樣本:5 組查詢向量(維度需與索引一致,此處以 128 維為例)
query_vectors = [
{"float32": [0.1] * 128},
{"float32": [0.2] * 128},
{"float32": [0.3] * 128},
{"float32": [0.4] * 128},
{"float32": [0.5] * 128},
]
batch_query(query_vectors)
運行後輸出:
查詢 0 完成,status code: 200
查詢 2 完成,status code: 200
查詢 1 完成,status code: 200
查詢 4 完成,status code: 200
查詢 3 完成,status code: 200
匯總結果已儲存到 query-results/sdk_batch_results.json
說明:由於線程池並發執行,輸出順序可能與提交順序不同,但最終結果按 query_id 排序儲存。
帶過濾條件的並發檢索
實際業務中,不同查詢可能需要搭配不同的過濾條件。以下樣本為每條查詢指定獨立的 filter:
import json
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
import alibabacloud_oss_v2 as oss
import alibabacloud_oss_v2.vectors as oss_vectors
ACCOUNT_ID = "<your-account-id>"
REGION = "cn-hangzhou"
BUCKET = "<your-vector-bucket>"
INDEX = "<your-index>"
MAX_CONCURRENT = 5
def create_vector_client():
"""建立向量檢索用戶端(全域複用,避免重複建立串連)"""
credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider()
cfg = oss.config.load_default()
cfg.credentials_provider = credentials_provider
cfg.region = REGION
cfg.account_id = ACCOUNT_ID
cfg.use_internal_endpoint = True # 如需通過公網訪問,請將此處設定為False或刪除此行
return oss_vectors.Client(cfg)
def run_query(client, query_vector, query_id, query_filter=None):
"""執行單條向量檢索"""
request = oss_vectors.models.QueryVectorsRequest(
bucket=BUCKET,
index_name=INDEX,
query_vector=query_vector,
filter=query_filter,
return_distance=True,
return_metadata=True,
top_k=10,
)
result = client.query_vectors(request)
print(f"查詢 {query_id} 完成,status code: {result.status_code}")
return {
"query_id": query_id,
"status_code": result.status_code,
"vectors": [str(v) for v in result.vectors] if result.vectors else [ ],
}
def batch_query(query_vectors):
"""批量並發執行多條向量檢索"""
client = create_vector_client()
results = [ ]
with ThreadPoolExecutor(max_workers=MAX_CONCURRENT) as executor:
futures = {
executor.submit(run_query, client, qv, idx): idx
for idx, qv in enumerate(query_vectors)
}
for future in as_completed(futures):
idx = futures[future]
try:
results.append(future.result())
except Exception as e:
print(f"查詢 {idx} 失敗: {e}")
results.append({"query_id": idx, "error": str(e)})
results.sort(key=lambda x: x["query_id"])
output_path = Path("./query-results/sdk_batch_results.json")
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(results, ensure_ascii=False, indent=2))
print(f"匯總結果已儲存到 {output_path}")
return results
if __name__ == "__main__":
tasks = [
{
"vector": {"float32": [0.1] * 128},
"filter": {"$and": [{"type": {"$in": ["tutorial"]}}]},
},
{
"vector": {"float32": [0.2] * 128},
"filter": {"$and": [{"type": {"$nin": ["comedy", "documentary"]}}]},
},
{
"vector": {"float32": [0.3] * 128},
"filter": None, # 不設過濾條件
},
]
client = create_vector_client()
results = [ ]
with ThreadPoolExecutor(max_workers=MAX_CONCURRENT) as executor:
futures = {
executor.submit(
run_query, client, t["vector"], idx, t["filter"]
): idx
for idx, t in enumerate(tasks)
}
for future in as_completed(futures):
try:
results.append(future.result())
except Exception as e:
print(f"查詢失敗: {e}")
for r in sorted(results, key=lambda x: x["query_id"]):
print(f"查詢 {r['query_id']}: 返回 {len(r.get('vectors', [ ]))} 條結果")
運行後輸出:
查詢 0 完成,status code: 200
查詢 1 完成,status code: 200
查詢 2 完成,status code: 200
查詢 0: 返回 10 條結果
查詢 1: 返回 10 條結果
查詢 2: 返回 10 條結果
Go SDK 並發檢索
使用 alibabacloud-oss-go-sdk-v2 Go SDK 通過 goroutine 並發調用 QueryVectors 介面。開始前請安裝 SDK:
go get github.com/aliyun/alibabacloud-oss-go-sdk-v2
確保已配置環境變數 OSS_ACCESS_KEY_ID 和 OSS_ACCESS_KEY_SECRET,並已建立向量 Bucket 和索引。
基本樣本
以下樣本使用 sync.WaitGroup 和 channel 訊號量並發檢索 5 組向量:
package main
import (
"context"
"fmt"
"log"
"sync"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/vectors"
)
const (
region = "cn-hangzhou"
bucketName = "<your-vector-bucket>"
accountId = "<your-account-id>"
indexName = "<your-index>"
maxConcurrent = 5
)
func main() {
cfg := oss.LoadDefaultConfig().
WithCredentialsProvider(credentials.NewEnvironmentVariableCredentialsProvider()).
WithRegion(region).
WithAccountId(accountId).
// 如需通過公網訪問,請將此處設定為false或刪除此行
WithUseInternalEndpoint(true)
client := vectors.NewVectorsClient(cfg)
// 5 組查詢向量(維度需與索引一致)
queryVectors := [ ]map[string]any{
{"float32": [ ]float32{0.1}},
{"float32": [ ]float32{0.2}},
{"float32": [ ]float32{0.3}},
{"float32": [ ]float32{0.4}},
{"float32": [ ]float32{0.5}},
}
var wg sync.WaitGroup
sem := make(chan struct{}, maxConcurrent) // channel 訊號量控制並發數
for i, qv := range queryVectors {
wg.Add(1)
sem <- struct{}{} // 擷取訊號量,達到上限時阻塞
go func(idx int, queryVector map[string]any) {
defer wg.Done()
defer func() { <-sem }() // 釋放訊號量
request := &vectors.QueryVectorsRequest{
Bucket: oss.Ptr(bucketName),
IndexName: oss.Ptr(indexName),
QueryVector: queryVector,
ReturnMetadata: oss.Ptr(true),
ReturnDistance: oss.Ptr(true),
TopK: oss.Ptr(10),
}
result, err := client.QueryVectors(context.TODO(), request)
if err != nil {
log.Printf("查詢 %d 失敗: %v", idx, err)
return
}
fmt.Printf("查詢 %d 完成,status code: %d\n",
idx, result.StatusCode)
}(i, qv)
}
wg.Wait()
fmt.Println("全部查詢完成")
}
運行後輸出:
查詢 0 完成,status code: 200
查詢 2 完成,status code: 200
查詢 1 完成,status code: 200
查詢 3 完成,status code: 200
查詢 4 完成,status code: 200
全部查詢完成
帶過濾條件的並發檢索
package main
import (
"context"
"fmt"
"log"
"sync"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/vectors"
)
type queryTask struct {
vector map[string]any
filter map[string]any
}
const (
region = "cn-hangzhou"
bucketName = "<your-vector-bucket>"
accountId = "<your-account-id>"
indexName = "<your-index>"
maxConcurrent = 5
)
func main() {
cfg := oss.LoadDefaultConfig().
WithCredentialsProvider(credentials.NewEnvironmentVariableCredentialsProvider()).
WithRegion(region).
WithAccountId(accountId).
// 如需通過公網訪問,請將此處設定為false或刪除此行
WithUseInternalEndpoint(true)
client := vectors.NewVectorsClient(cfg)
tasks := []queryTask{
{
vector: map[string]any{"float32": []float32{0.1}},
filter: map[string]any{
"$and": []map[string]any{
{"type": map[string]any{"$in": []string{"tutorial"}}},
},
},
},
{
vector: map[string]any{"float32": []float32{0.2}},
filter: map[string]any{
"$and": []map[string]any{
{"type": map[string]any{"$nin": []string{"comedy", "documentary"}}},
},
},
},
{
vector: map[string]any{"float32": []float32{0.3}},
filter: nil, // 不設過濾條件
},
}
var wg sync.WaitGroup
sem := make(chan struct{}, maxConcurrent)
for i, task := range tasks {
wg.Add(1)
sem <- struct{}{}
go func(idx int, t queryTask) {
defer wg.Done()
defer func() { <-sem }()
request := &vectors.QueryVectorsRequest{
Bucket: oss.Ptr(bucketName),
IndexName: oss.Ptr(indexName),
QueryVector: t.vector,
ReturnMetadata: oss.Ptr(true),
ReturnDistance: oss.Ptr(true),
TopK: oss.Ptr(10),
}
if t.filter != nil {
request.Filter = t.filter
}
result, err := client.QueryVectors(context.TODO(), request)
if err != nil {
log.Printf("查詢 %d 失敗: %v", idx, err)
return
}
fmt.Printf("查詢 %d 完成,status code: %d\n", idx, result.StatusCode)
}(i, task)
}
wg.Wait()
fmt.Println("全部查詢完成")
}
運行後輸出:
查詢 0 完成,status code: 200
查詢 1 完成,status code: 200
查詢 2 完成,status code: 200
全部查詢完成
並發效能調優
|
調優項 |
建議 |
說明 |
|
並發數 |
3~5 |
|
|
|
按需設定 |
返回結果越多,單次請求延遲越高。僅返回業務所需的數量 |
|
錯誤重試 |
間隔 1~2 秒 |
並發請求可能觸發限流(HTTP 429),建議捕獲錯誤後等待重試 |
|
CLI 結果輸出 |
重新導向到檔案 |
多個進程同時輸出到終端會導致內容交錯,建議將每條結果寫入獨立檔案 |
|
SDK 用戶端複用 |
複用同一執行個體 |
避免為每條查詢建立新的 |