PolarSearch的向量检索功能的支持通过REST API对文本、图像等非结构化数据进行高效的相似度搜索,能够在海量数据中快速而精准地找到与查询目标最为相似的结果,从而有效提升应用的智能化水平
功能简介
向量检索,也称为相似性搜索,是一种通过比较向量之间的“距离”来查找最相似数据的技术,它与依赖精确关键词匹配的传统搜索有着本质区别。
它的核心思想是将现实世界中的文本、图像、音频等非结构化数据,通过深度学习模型(如LLM)转换为向量嵌入(Embedding) 的数值表示。这些多维向量能够捕捉数据的深层语义信息。
当您发起一次查询时,PolarSearch会将您的查询内容同样转换为向量,然后执行k-最近邻(k-Nearest Neighbor, k-NN)搜索。这是一种核心算法,其目标是在海量数据中找到与您的查询向量“距离”最近的k个向量。这里的k是一个由您定义的数字(例如,当k=5时,即代表查找最相似的5个结果)。最终,PolarSearch会返回这k个最相似的结果。
为实现高效检索,PolarSearch依赖两大核心组件:向量索引和向量存储优化。
向量索引:为了避免在海量数据中进行全量计算,需要预先建立向量索引。索引能够根据向量数据的特征构建一种为查询而优化的数据结构,在查询时能够大幅缩小搜索范围,从而显著提升检索性能。PolarSearch内支持了多种类型的向量索引,以下为您主要介绍业界主流的HNSW和IVF索引:
HNSW (Hierarchical Navigable Small World):一种基于图的索引,具有高性能和高召回率的优点,但内存开销也相应较大。适用于对查询延迟和精度要求极高,且数据集大小在内存容量范围内的场景。
IVF (Inverted File):一种基于聚类的倒排索引,内存占用较低,更适合需要处理超大规模数据集且内存受限的场景,但其搜索精度通常略低于HNSW。
向量存储优化:向量数据,尤其是高维向量,会占用大量内存和存储空间。PolarSearch提供多种优化技术来降低资源消耗。
向量量化:通过降低向量数值的精度来压缩数据,显著减少空间占用,是一种在压缩率和精度之间取得平衡的技术。PolarSearch支持乘积量化(PQ)、标量量化(SQ)和二值量化(BQ)。
基于磁盘的存储:对于低内存环境,允许将部分索引数据存储在磁盘上,以较低的内存成本运行向量检索服务,代价是会适当增加查询延迟。
注意事项
在使用PolarSearch向量检索功能时,请您注意以下几点:
索引训练要求:
IVF索引和PQ(乘积量化)技术在使用前需要一个独立的训练步骤。您需要提供一部分具有代表性的向量数据来训练模型,否则索引无法正常工作。内存开销:
HNSW索引虽然性能优异,但其图结构需要完全加载到内存中,会产生较高的内存开销。请在选择前评估您的集群内存资源。性能与成本权衡:基于磁盘的向量搜索会适当增加查询延迟,请根据您的业务场景进行评估。
自动训练:二值量化(BQ)的训练过程在索引构建期间自动处理,您无需进行额外的训练操作。
操作指南
准备工作
要使用REST API进行向量检索,您需要先开启智能搜索(PolarSearch)功能。请参考PolarSearch使用说明为已有集群或新建集群开启PolarSearch功能。
步骤一:创建向量索引
要存储和搜索向量,您需首先创建一个特定配置的索引。这主要包含两个关键动作:
启用k-NN并定义向量字段:在索引的设置(
settings)中将knn参数设为true。这是一个主开关,它告知PolarDB该索引将用于向量检索。核心参数
engine:固定为faiss。说明Faiss (Facebook AI Similarity Search) 是一个由Meta AI开发的高性能开源库,专门用于高效的相似度搜索和海量向量数据聚类,PolarSearch使用Faiss作为其核心向量检索引擎。
dimension:用于指定向量的维度,需要与您模型产出的向量维度完全一致。data_type:定义向量的数据类型。默认为float,您也可以选择byte或binary以优化存储。space_type:定义向量相似度的计算方式(距离度量)。支持的范围如下:space_type距离度量
说明
l2L2(欧几里得距离)
计算平方差和的平方根,对数值大小敏感。
l1L1(曼哈顿距离)
对向量各维度差值的绝对值求和。
cosinesimil余弦相似度
测量向量间的夹角,更关注方向而非大小。
innerproduct内积
计算向量点积,常用于排序场景。
hamming汉明距离
计算二进制向量中不同元素的数量。
chebyshevL∞(切比雪夫距离)
仅考虑向量各维度差值绝对值的最大值。
定义向量字段(HNSW或IVF):在索引的映射(
mappings)中,需定义一个knn_vector类型的字段。这个字段专门用于存储向量数据,并在此处配置向量的维度、相似度计算方式以及核心的索引方法。选型建议
HNSW和IVF在性能、资源消耗和精度上各有侧重,适用于不同的业务场景。您可以参考下表进行快速选型:
对比维度
HNSW
IVF
查询延迟
极低。通过层级化的图结构快速定位,搜索路径短。
较低。需要先定位到簇,再在簇内搜索,路径相对较长。
召回率(精度)
高。图的连接性更好,不容易漏掉近邻点。
中到高。存在边缘效应(查询点在簇的边界),可能损失一定精度,可通过调整
nprobes参数缓解。内存占用
高。需要将完整的图结构加载到内存中。
低。主要存储聚类中心和倒排列表,内存开销远低于HNSW。
构建时间
较长。构建高质量的图结构需要复杂的计算。
较快。但需要一个额外的训练步骤来生成聚类中心。
适用场景
对查询性能和精度有极致要求,且内存资源充足的场景。例如:实时语义搜索、人脸识别。
数据集规模巨大,内存资源受限,且可以接受微小精度损失的成本敏感型场景。例如:海量商品推荐、大规模图片库检索。
使用示例
HNSW
HNSW通过IndexHNSWFlat实现,适用于对性能和召回率有高要求的场景。
核心参数
参数 | 取值范围 | 说明 |
| 正整数。 | 图中每个节点的最大邻居(出度)数量。此值决定了图的密度,且是影响索引质量和内存占用的最关键参数。
|
| 正整数,且通常应大于 | 构建索引时,动态邻居列表的大小。它控制了构建图期间的搜索深度和广度。此值主要影响索引的构建时间和最终质量。
|
| 正整数。 | 查询时,动态邻居列表的大小。它控制了查询期间的搜索深度。 说明 此参数不在创建索引时指定,而是在查询时或在索引的
|
实际创建HNSW索引时,请将下述<my-index>替换为您的索引名称,<my_vector_field>替换为您的字段名称。同时,其他核心参数dimension、data_type、space_type、m以及ef_construction等参数请根据实际业务需求配置。
REST API
// HNSW索引创建示例,请将<my-index>替换为您的实际的索引名称
PUT /<my-index>
{
"settings": {
"index": {
"knn": true
}
},
"mappings": {
"properties": {
"<my_vector_field>": {//请将<my_vector_field>替换为您的实际的字段名称
"type": "knn_vector",
"dimension": 128,
"data_type": "float",
"method": {
"name": "hnsw",
"engine": "faiss",
"space_type": "l2",
"parameters": {
"m": 16,
"ef_construction": 256
}
}
}
}
}
}Java client
private static void createVectorIndex(OpenSearchClient client) throws IOException {
Property vectorProperty = Property.of(p -> p.knnVector(
KnnVectorProperty.of(kvp -> kvp
.dimension(128)
.dataType("float")
.method(new KnnVectorMethod.Builder()
.name("hnsw")
.engine("faiss")
.spaceType("l2")
.parameters(Map.of(
"m", JsonData.of(16),
"ef_construction", JsonData.of(256)
))
.build()
)
)
));
TypeMapping mapping = TypeMapping.of(m -> m
.properties("<my_vector_field>", vectorProperty)
.properties("text", Property.of(p -> p.text(TextProperty.of(t -> t))))
.properties("category", Property.of(p -> p.keyword(k -> k)))
);
CreateIndexRequest request = new CreateIndexRequest.Builder()
.index(<my-index>)
.settings(s -> s.knn(true))
.mappings(mapping)
.build();
client.indices().create(request);
}IVF
IVF通过IndexIVFFlat实现,适用于内存受限的超大规模数据集场景。
核心参数
参数 | 取值范围 | 说明 |
| 正整数。 | 聚类中心的数量。索引会将整个向量空间划分为
|
| 正整数,且通常应小于 | 查询时,需要搜索的聚类中心(簇)的数量。此值是在查询速度和召回率之间进行权衡的最直接参数。
|
实际创建IVF索引时,请将下述<my-index>替换为您的索引名称,<my_vector_field>替换为您的字段名称。同时,其他核心参数dimension、data_type、space_type、nlist、nprobes等参数请根据实际业务需求配置。
// IVF索引创建示例,请将<my-index>替换为您的实际的索引名称
PUT /<my-index>
{
"settings": {
"index": {
"knn": true
}
},
"mappings": {
"properties": {
"<my_vector_field>": {// 请将<my_vector_field>替换为您的实际的字段名称
"type": "knn_vector",
"dimension": 4,
"data_type": "byte",
"method": {
"name": "ivf",
"engine": "faiss",
"space_type": "l2",
"parameters": {
"nlist": 1024,
"nprobes": 10 // nprobes通常在查询时指定,此处为示例
}
}
}
}
}
}步骤二:索引向量数据
准备好您的文档,包括向量数据及其他元数据,然后将其索引到您刚刚创建的索引中。
REST API
POST /_bulk
{ "index": { "_index": "my-index", "_id": "doc_1" } }
{ "my_vector_field": [5.2, 4.4] }
{ "index": { "_index": "my-index", "_id": "doc_2" } }
{ "my_vector_field": [5.2, 3.9] }
{ "index": { "_index": "my-index", "_id": "doc_3" } }
{ "my_vector_field": [4.9, 3.4] }
{ "index": { "_index": "my-index", "_id": "doc_4" } }
{ "my_vector_field": [4.2, 4.6] }
{ "index": { "_index": "my-index", "_id": "doc_5" } }
{ "my_vector_field": [3.3, 4.5] }Java client
private static void indexSampleData(OpenSearchClient client) throws IOException {
List<Map<String, Object>> documents = new ArrayList<>();
documents.add(Map.of("text", "a book about data science", "category", "books", "<my_vector_field>", List.of(1.0f, 2.0f, 3.0f, 4.0f)));
documents.add(Map.of("text", "an intelligent smartphone with a great camera", "category", "electronics", "<my_vector_field>", List.of(8.0f, 7.0f, 6.0f, 5.0f)));
documents.add(Map.of("text", "a technical manual for a smart device", "category", "electronics", "<my_vector_field>", List.of(3.0f, 4.0f, 5.0f, 6.0f)));
for (int i = 0; i < documents.size(); i++) {
IndexRequest<Map<String, Object>> request = new IndexRequest.Builder<Map<String, Object>>()
.index(<my-index>)
.id("doc_" + i)
.document(documents.get(i))
.build();
client.index(request);
}
}步骤三:执行向量检索
现在您可以发起向量检索请求,从海量数据中找出与查询向量最相似的结果。
基本k-NN搜索
这是最基础的向量搜索,它会在整个索引中找出与查询向量距离最近的K个结果。
REST API
POST /<my-index>/_search
{
"size": 3,
"query": {
"knn": {
"<my_vector_field>": {
"vector": [3.1, 4.1, 5.1, 6.1],
"k": 3
}
}
}
}Java client
// 准备您的查询向量
List<Float> queryVector = List.of(3.1f, 4.1f, 5.1f, 6.1f);
private static void performBasicKnnSearch(OpenSearchClient client, List<Float> queryVector) throws IOException {
System.out.println("\n--- 1. Performing Basic k-NN Search ---");
System.out.println("Querying for vectors most similar to: " + queryVector);
// 查找最相似的3个结果
int k = 3;
KnnQuery knnQuery = new KnnQuery.Builder()
.field("<my_vector_field>")
.vector(queryVector)
.k(k)
.build();
SearchRequest searchRequest = new SearchRequest.Builder().index(<my-index>).query(new Query.Builder().knn(knnQuery).build()).size(k).build();
SearchResponse<Map> response = client.search(searchRequest, Map.class);
printResults(response);
}带过滤条件的k-NN搜索(混合搜索)
在许多场景下,您需要在执行向量搜索前,先通过一个或多个条件缩小搜索范围。这正是混合搜索的核心思想。您可以在KnnQuery中使用filter子句来实现这一点,过滤器本身可以是任何标准的OpenSearch查询,如term(精确值匹配)或match(全文检索)。
使用文本匹配过滤
这适用于经典的“关键字+向量”混合搜索场景。例如,先搜索所有描述中包含“新款手机”的文档,然后根据向量相似度对它们进行排序。
REST API
POST /<my-index>/_search
{
"size": 3,
"query": {
"knn": {
"<my_vector_field>": {
"vector": [3.1, 4.1, 5.1, 6.1],
"k": 3,
"filter": {
"match": {
"text": "book"
}
}
}
}
}
}Java client
// 准备您的查询向量
List<Float> queryVector = List.of(3.1f, 4.1f, 5.1f, 6.1f);
private static void performHybridSearchWithText(OpenSearchClient client, List<Float> queryVector) throws IOException {
System.out.println("\n--- 2. Performing Hybrid Search (k-NN + Text Match) ---");
// 准备您的查询关键字
String textQuery = "book";
System.out.println("Filtering for documents containing '" + textQuery + "', then finding most similar vectors.");
// 查找最相似的3个结果
int k = 3;
MatchQuery matchQuery = new MatchQuery.Builder().field("text").query(q -> q.stringValue(textQuery)).build();
KnnQuery hybridKnnQuery = new KnnQuery.Builder()
.field("<my_vector_field>")
.vector(queryVector)
.k(k)
.filter(new Query.Builder().match(matchQuery).build())
.build();
SearchRequest searchRequest = new SearchRequest.Builder().index(<my-index>).query(new Query.Builder().knn(hybridKnnQuery).build()).size(k).build();
SearchResponse<Map> response = client.search(searchRequest, Map.class);
printResults(response);
}使用精确值过滤(Term Filter)
这适用于根据明确的标签、分类或ID进行过滤的场景。例如,只在“电子产品”类别中搜索最相似的商品。
REST API
POST /<my-index>/_search
{
"size": 3,
"query": {
"knn": {
"<my_vector_field>": {
"vector": [5, 4],
"k": 3,
"filter": {
"term": {
"category": "electronics"
}
}
}
}
}
}Java client
// 准备您的查询向量
List<Float> queryVector = List.of(3.1f, 4.1f, 5.1f, 6.1f);
private static void performFilteredSearchWithTerm(OpenSearchClient client, List<Float> queryVector) throws IOException {
System.out.println("\n--- 3. Performing Filtered Search (k-NN + Term Filter) ---");
// 准备您的查询分类
String categoryFilter = "electronics";
System.out.println("Filtering for documents in category '" + categoryFilter + "', then finding most similar vectors.");
// 查找最相似的3个结果
int k = 3;
TermQuery termQuery = new TermQuery.Builder().field("category").value(v -> v.stringValue(categoryFilter)).build();
KnnQuery filteredKnnQuery = new KnnQuery.Builder()
.field("<my_vector_field>")
.vector(queryVector)
.k(k)
.filter(new Query.Builder().term(termQuery).build())
.build();
SearchRequest searchRequest = new SearchRequest.Builder().index(<my-index>).query(new Query.Builder().knn(filteredKnnQuery).build()).size(k).build();
SearchResponse<Map> response = client.search(searchRequest, Map.class);
printResults(response);
}配置存储优化
向量数据,尤其是高维浮点型向量,会占用大量内存。PolarSearch提供多种存储优化技术,通过对向量进行压缩(量化)或改变存储介质,在内存成本、查询性能和搜索精度之间取得平衡。
选型建议
在选择具体的优化策略前,您可以参考下表,快速找到最适合您业务场景的方案。
优化策略 | 压缩率 | 精度影响 | 训练要求 | CPU开销 | 适用场景 |
标量量化 (SQ) | 低 (固定2倍) | 极小 | 无需训练 | 低 | 对搜索精度要求极高,希望在几乎不损失精度的前提下,获得适度内存优化的场景。 |
二值量化 (BQ) | 高 (8-32倍) | 较大 | 无需训练 | 中等 | 对内存极度敏感,可以接受一定(甚至较大)精度损失,以换取最大程度内存节省的场景。 |
乘积量化 (PQ) | 最高 | 中等 | 需要训练 | 中等 | 数据集巨大,需要极致的压缩率,且愿意投入时间进行模型训练以平衡精度和内存的场景。 |
基于磁盘的向量存储 | - | 较大 | 无需训练 | 较高 | 内存资源极其有限,宁愿牺牲查询延迟(因磁盘I/O),也要将内存占用降至最低的成本敏感型场景。 |
操作说明
标量量化(SQ)
工作原理:将标准的32位浮点(float)向量转换为16位浮点(fp16)向量进行存储,使内存占用直接减半。在计算距离时,会解码回32位进行,因此对精度的影响非常小。
内存估算:
公式:
内存 (GB) ≈ 1.1 * (2 * dimension + 8 * m) * num_vectors / 1024^3参数详解:
dimension:向量的维度。m:HNSW索引中的m参数,即每个节点的最大邻居数。num_vectors:向量总数。1.1:约10%的系统开销系数。
示例:假设您有 100 万个向量,每个向量的维度为 256,每个向量的维度
m为 16。内存需求可以估算如下:1.1 * (2 * 256 + 8 * 16) * 1,000,000 ~= 0.656 GB
使用示例:
// HNSW + 标量量化(SQ)示例 PUT /<my-sq-index> { "settings": { "index": { "knn": true } }, "mappings": { "properties": { "<my_vector_field>": { "type": "knn_vector", "dimension": 128, "method": { "name": "hnsw", "engine": "faiss", "parameters": { "m": 16, "ef_construction": 256, "encoder": {// 启用SQ "name": "fp16" } } } } } } }
二值量化(BQ)
工作原理:将浮点向量的每个维度压缩为二进制位(0和1)进行存储,从而实现极高的压缩率。训练过程在索引构建时自动完成。
内存估算:
公式:
内存 (GB) ≈ 1.1 * ((dimension * bits / 8) + 8 * m) * num_vectors / 1024^3参数详解:
dimension:向量的维度。bits:每个维度用多少个二进制位表示,可选值为1,2,4。bits越小,压缩率越高,但精度损失越大。m:HNSW索引中的m参数。num_vectors:向量总数。
示例:假设您有100万个向量,每个向量的维度为256,每个向量的维度
m为16。以下部分提供了各种压缩值对内存需求的估算。1位量化(32倍压缩):在1位量化中,每个维度用1位表示,相当于32倍压缩系数。内存需求可以估算如下:
1.1 * ((256 * 1 / 8) + 8 * 16) * 1,000,000 ~= 0.176 GB2位量化(16倍压缩):在2位量化中,每个维度用2位表示,相当于16倍压缩系数。内存需求可以估算如下:
1.1 * ((256 * 2 / 8) + 8 * 16) * 1,000,000 ~= 0.211 GB
使用示例:
// HNSW + 二值量化(BQ)示例 PUT /<my-bq-index> { "settings" : { "index": { "knn": true } }, "mappings": { "properties": { "<my_vector_field>": { "type": "knn_vector", "dimension": 128, "method": { "name": "hnsw", "engine": "faiss", "parameters": { "m": 16, "ef_construction": 512, "encoder": { "name": "binary", "parameters": {// 启用BQ,使用1位量化 "bits": 1 } } } } } } } }
乘积量化(PQ)
PQ是一种先进的向量压缩技术,它能实现比SQ或BQ更高的压缩率,但代价是需要一个独立的训练步骤来构建压缩模型。
工作原理:
向量切分:首先,将一个原始的高维向量(例如256维)切分为
m个等长的低维子向量。例如,将256维向量按m=32切分,会得到32个8维的子向量。码本训练:接着,系统会为每一个子向量空间独立学习一个“码本”(Codebook)。这个码本包含
2^code_size个中心点(也称质心)。这个训练过程通常使用K-均值聚类算法完成。量化编码:训练完成后,在对新向量进行编码时,其每个子向量不再存储原始的浮点值,而是被替换为该子向量空间码本中距离它最近的那个中心点的ID。如果
code_size为8,则ID范围是0-255,正好用1个字节存储。最终结果:一个原始向量就被转换成了一组中心点ID的序列,从而实现了极高的压缩。
训练要求:PQ的性能严重依赖于训练数据的质量。您必须提供一组与您最终要检索的数据分布相似的向量来进行训练。
训练数据来源:可以是您要索引的向量数据的子集。
建议的训练数据量:
结合HNSW使用时:建议训练向量数量为
2^code_size * 1000。结合IVF使用时:建议训练向量数量为
max(1000 * nlist, 2^code_size * 1000)。
内存估算:以HNSW+PQ为例。因为当HNSW与PQ结合使用时,其内存计算公式较为复杂,因为它包含了压缩向量、HNSW图结构和PQ码本三部分的开销。
公式:
内存 (字节) ≈ 1.1 * ( (per_vector_cost) * num_vectors + (codebook_cost) )per_vector_cost = (pq_code_size / 8 * pq_m) + 24 + (8 * hnsw_m)codebook_cost = num_segments * (2^pq_code_size) * 4 * dimension
参数详解:
num_vectors:向量总数。dimension:原始向量的维度。pq_m:向量切分的段数。dimension必须能被pq_m整除。pq_code_size:每个子向量码本的大小,以比特为单位。通常为8。hnsw_m:HNSW索引中的m参数,即每个节点的最大邻居数。num_segments:一个底层技术参数,代表索引被分成的段数。在估算时可以按集群分片数或一个保守值(如100)来计算。1.1:约10%的系统开销系数。24和8:HNSW图结构中每个节点的固定开销和指针开销。4:代表码本中的中心点坐标使用32位浮点数(4字节)存储。
示例:假设您有100万个向量(
num_vectors),每个向量的维度(dimension)为256,每个向量切分段数(pq_m)为32,每个子向量码本的大小(pq_code_size)为8,HNSW索引的m参数为16,num_segments为100。计算单个向量的开销(
per_vector_cost):压缩后向量大小 = pq_code_size / 8 * pq_m = 8 / 8 * 32 = 32字节。
HNSW图开销 = 24 + 8 * hnsw_m = 24 + 8 * 16 = 152字节。
per_vector_cost = 32 + 152 = 184字节
计算码本的总开销(
codebook_cost):codebook_cost = num_segments * (2^pq_code_size) * 4 * dimension。
codebook_cost = 100 * (2^8) * 4 * 256 = 100 * 256 * 4 * 256 = 26,214,400字节。
计算总内存:
总内存 ≈ 1.1 * (per_vector_cost * num_vectors + codebook_cost)
总内存 ≈ 1.1 * (184 * 1,000,000 + 26,214,400) ≈ 231,235,840 字节 ≈ 0.215 GB
使用示例:
// HNSW + 乘积量化(PQ)示例 PUT /<my-hnswpq-index> { "settings" : { "index": { "knn": true } }, "mappings": { "properties": { "<my_vector_field>": { "type": "knn_vector", "dimension": 128, // 维度必须能被 m 整除 "method": { "name": "hnsw", "engine": "faiss", "parameters": { "m": 16, // HNSW的m参数 "ef_construction": 512, "encoder": { "name": "pq", "parameters": { "m": 4, // PQ的m参数:将128维切为4段32维 "code_size": 8 } } } } } } } }
基于磁盘的向量存储
工作原理:基于磁盘的向量搜索是利用内部的量化技术压缩向量,并将主要的图结构存储在磁盘上,而不是堆内存中。这种内存优化可以大幅节省内存,但搜索延迟会略有增加,同时仍能保持较高的召回率。
内存估算:无固定公式。实际物理内存占用由操作系统根据访问模式动态管理。
使用示例:
// 基于磁盘存储的示例 PUT /<my-ondisk-index> { "settings" : { "index": { "knn": true } }, "mappings": { "properties": { "<my_vector_field>": { "type": "knn_vector", "dimension": 128, "mode": "on_disk" // 启用基于磁盘的模式 } } } }
附录:完整示例代码
下面是一个完整的Java client示例代码,演示了从创建向量索引到执行向量检索的全过程。
运行方式
mvn clean package
mvn exec:java -Dexec.mainClass="com.example.VectorSearchDemo"