0

Qdrant 1.10 - 通用查询、内置 IDF 及 ColBERT 支持

David Myriel

·

2024 年 7 月 1 日

Qdrant 1.10 - Universal Query, Built-in IDF & ColBERT Support

Qdrant 1.10.0 现已发布! 该版本引入了多项重大变更,让我们深入了解一下。

通用查询 API (Universal Query API):包括混合搜索在内的所有搜索 API 现在统一到一个 Query 接口中。
内置 IDF:我们在 Qdrant 的核心搜索和索引流程中加入了 IDF 机制。
多向量支持:通过 Query API 可实现对 ColBERT 后期交互(late interaction)的原生支持。

所有查询共用一个接口

Query API 将所有搜索 API 合并为单个请求。以前,您需要在此 API 之外处理不同搜索请求的组合。现在,这些方法被简化为单个请求的参数,无需再手动合并各个结果。

您现在可以使用以下参数配置 Query API 请求:

参数描述
无参数id 返回点位
nearest查询最近邻(搜索
fusion融合稀疏/稠密预取查询(混合搜索
discover查询带有附加 contexttarget发现 API
context仅有 context 而无 target 的查询(上下文搜索
recommend基于 positive/negative 示例进行查询(推荐 API
order_by有效负载 (payload) 字段对结果进行排序

例如,您可以配置 Query API 来运行发现搜索 (Discovery search)。让我们看看具体效果。

POST collections/{collection_name}/points/query
{
  "query": {
    "discover": {
      "target": <vector_input>,
      "context": [
        {
          "positive": <vector_input>,
          "negative": <vector_input>
        }
      ]
    }
  }
}

我们将在文档和新的API 规范中发布代码示例。
如果您需要关于此新方法的额外支持,可以通过我们的 Discord 向工程师寻求帮助。

原生混合搜索支持

Query API 现在还原生支持稀疏/稠密融合。在此之前,您必须自行合并稀疏和稠密搜索的结果。现在,这在后端即可完成,您只需将其作为 Query API 的基础参数进行配置即可。

POST /collections/{collection_name}/points/query
{
    "prefetch": [
        {
            "query": { 
                "indices": [1, 42],    // <┐
                "values": [0.22, 0.8]  // <┴─sparse vector
             },
            "using": "sparse",
            "limit": 20
        },
        {
            "query": [0.01, 0.45, 0.67, ...], // <-- dense vector
            "using": "dense",
            "limit": 20
        }
    ],
    "query": { "fusion": "rrf" }, // <--- reciprocal rank fusion
    "limit": 10
}
from qdrant_client import QdrantClient, models

client = QdrantClient(url="https://:6333")

client.query_points(
    collection_name="{collection_name}",
    prefetch=[
        models.Prefetch(
            query=models.SparseVector(indices=[1, 42], values=[0.22, 0.8]),
            using="sparse",
            limit=20,
        ),
        models.Prefetch(
            query=[0.01, 0.45, 0.67],
            using="dense",
            limit=20,
        ),
    ],
    query=models.FusionQuery(fusion=models.Fusion.RRF),
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

client.query("{collection_name}", {
    prefetch: [
        {
            query: {
                values: [0.22, 0.8],
                indices: [1, 42],
            },
            using: 'sparse',
            limit: 20,
        },
        {
            query: [0.01, 0.45, 0.67],
            using: 'dense',
            limit: 20,
        },
    ],
    query: {
        fusion: 'rrf',
    },
});
use qdrant_client::Qdrant;
use qdrant_client::qdrant::{Fusion, PrefetchQueryBuilder, Query, QueryPointsBuilder};

let client = Qdrant::from_url("https://:6334").build()?;

client.query(
    QueryPointsBuilder::new("{collection_name}")
        .add_prefetch(PrefetchQueryBuilder::default()
            .query(Query::new_nearest([(1, 0.22), (42, 0.8)].as_slice()))
            .using("sparse")
            .limit(20u64)
        )
        .add_prefetch(PrefetchQueryBuilder::default()
            .query(Query::new_nearest(vec![0.01, 0.45, 0.67]))
            .using("dense")
            .limit(20u64)
        )
        .query(Query::new_fusion(Fusion::Rrf))
).await?;
import static io.qdrant.client.QueryFactory.nearest;

import java.util.List;

import static io.qdrant.client.QueryFactory.fusion;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.Fusion;
import io.qdrant.client.grpc.Points.PrefetchQuery;
import io.qdrant.client.grpc.Points.QueryPoints;

QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client.queryAsync(
    QueryPoints.newBuilder()
    .setCollectionName("{collection_name}")
    .addPrefetch(PrefetchQuery.newBuilder()
      .setQuery(nearest(List.of(0.22f, 0.8f), List.of(1, 42)))
      .setUsing("sparse")
      .setLimit(20)
      .build())
    .addPrefetch(PrefetchQuery.newBuilder()
      .setQuery(nearest(List.of(0.01f, 0.45f, 0.67f)))
      .setUsing("dense")
      .setLimit(20)
      .build())
    .setQuery(fusion(Fusion.RRF))
    .build())
  .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.QueryAsync(
  collectionName: "{collection_name}",
  prefetch: new List < PrefetchQuery > {
    new() {
      Query = new(float, uint)[] {
          (0.22f, 1), (0.8f, 42),
        },
        Using = "sparse",
        Limit = 20
    },
    new() {
      Query = new float[] {
          0.01f, 0.45f, 0.67f
        },
        Using = "dense",
        Limit = 20
    }
  },
  query: Fusion.Rrf
);

Query API 现在可以预取请求向量,这意味着您可以在同一个 API 调用中按顺序执行多个查询。这里有许多选项,您需要定义一个策略来使用新参数合并这些请求。例如,现在可以在混合搜索中包含重排序 (rescoring),这为通过 Matryoshka 嵌入实现迭代优化等策略提供了可能。

要了解更多信息,请阅读 Query API 文档

逆文档频率 [IDF]

IDF 是 TF-IDF(词频-逆文档频率)加权方案的关键组件,用于评估单词在文档相对于文档集合(语料库)中的重要性。IDF 的计算方法有多种,但最常用的公式是:

$$ \text{IDF}(q_i) = \ln \left(\frac{N - n(q_i) + 0.5}{n(q_i) + 0.5}+1\right) $$

其中
N 是集合中的文档总数。
n 是给定向量值非零的文档数量。

此变体也用于 BM25,该功能深受用户期待。我们决定将 IDF 计算移入 Qdrant 引擎内部。这种分离方式允许在进行 IDF 计算的同时,对稀疏嵌入进行流式更新。

以前,IDF 值必须在客户端利用所有文档进行计算。现在 Qdrant 开箱即用支持该功能,您无需在其他地方实现该逻辑,也不必在文档增删后重新计算该值。

您可以在集合配置中启用 IDF 修改器。

PUT /collections/{collection_name}
{
    "sparse_vectors": {
        "text": {
            "modifier": "idf"
        }
    }
}
from qdrant_client import QdrantClient, models
client = QdrantClient(url="https://:6333")
client.create_collection(
    collection_name="{collection_name}",
    sparse_vectors={
        "text": models.SparseVectorParams(
            modifier=models.Modifier.IDF,
        ),
    },
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

client.createCollection("{collection_name}", {
    sparse_vectors: {
        "text": {
            modifier: "idf"
        }
    }
});
use qdrant_client::Qdrant;
use qdrant_client::qdrant::{CreateCollectionBuilder, sparse_vectors_config::SparseVectorsConfigBuilder, Modifier, SparseVectorParamsBuilder};

let client = Qdrant::from_url("https://:6334").build()?;

let mut config = SparseVectorsConfigBuilder::default();
config.add_named_vector_params(
    "text",
    SparseVectorParamsBuilder::default().modifier(Modifier::Idf),
);

client
    .create_collection(
        CreateCollectionBuilder::new("{collection_name}")
            .sparse_vectors_config(config),
    )
    .await?;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateCollection;
import io.qdrant.client.grpc.Collections.Modifier;
import io.qdrant.client.grpc.Collections.SparseVectorConfig;
import io.qdrant.client.grpc.Collections.SparseVectorParams;

QdrantClient client =
  new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
  .createCollectionAsync(
    CreateCollection.newBuilder()
    .setCollectionName("{collection_name}")
    .setSparseVectorsConfig(
      SparseVectorConfig.newBuilder()
      .putMap("text", SparseVectorParams.newBuilder().setModifier(Modifier.Idf).build()))
    .build())
  .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.CreateCollectionAsync(
  collectionName: "{collection_name}",
  sparseVectorsConfig: ("text", new SparseVectorParams {
    Modifier = Modifier.Idf,
  })
);

作为 BM42 核心的 IDF

本季度,Qdrant 还引入了 BM42,这是一种结合了 BM25 的 IDF 元素与基于 Transformer 的注意力矩阵的新颖算法,旨在提高文本检索效果。它利用来自嵌入模型的注意力矩阵,根据文档中每个 Token 获得的注意力值来确定其重要性。

我们准备了标准的 all-MiniLM-L6-v2 Sentence Transformer,使其输出注意力值。当然,只要您能访问模型的参数,几乎可以使用任何您选择的模型。这也是选择开源技术而非专有系统的一个理由。

实际上,BM42 方法解决了与 SPLADE 相关的 Token 化问题和计算成本。该模型在处理不同类型和长度的文档时既高效又有效,通过结合 BM25 和现代 Transformer 技术的优势,提供了增强的搜索性能。

要了解关于 IDF 和 BM42 的更多信息,请阅读我们专门的技术文章

您可以预期 BM42 在以短文本为主的可扩展 RAG 场景中表现卓越。 BM42 的文档推理速度快得多,这对于搜索引擎、推荐系统和实时决策系统等大规模应用至关重要。

多向量支持

我们正在添加对多向量搜索的原生支持,它兼容如 ColBERT 模型等后期交互 (late-interaction) 技术。如果您正在处理高维相似度搜索,强烈建议将 ColBERT 作为通用查询搜索中的重排序步骤。 由于 ColBERT 的方法允许更深入的语义理解,您将获得更高质量的向量检索。

该模型在查询-文档交互期间保留了上下文信息,从而获得更好的相关性评分。在效率和可扩展性方面,文档和查询将分开编码,这为预计算和存储文档嵌入以实现更快的检索提供了机会。

注意: 该功能支持所有原始量化压缩方法,与普通搜索方法相同。

运行带有 ColBERT 向量的查询。

Query API 可以处理极其复杂的请求。以下示例使用 mrl_byte 命名向量预取与给定查询最相似的 1000 个条目,然后对其进行重排序以获取匹配度最高的 100 个结果,最后再次重排序以提取名为 colbert 的命名向量的前 10 个结果。现在,单个 API 调用即可实现复杂的重排序方案。

POST /collections/{collection_name}/points/query
{
    "prefetch": {
        "prefetch": {
            "query": [1, 23, 45, 67], // <------ small byte vector
            "using": "mrl_byte",
            "limit": 1000
        },
        "query": [0.01, 0.45, 0.67, ...], // <-- full dense vector
        "using": "full",
        "limit": 100
    },
    "query": [           // <─┐
        [0.1, 0.2, ...], // < │
        [0.2, 0.1, ...], // < ├─ multi-vector
        [0.8, 0.9, ...]  // < │
    ],                   // <─┘       
    "using": "colbert",
    "limit": 10
}
from qdrant_client import QdrantClient, models

client = QdrantClient(url="https://:6333")

client.query_points(
    collection_name="{collection_name}",
    prefetch=models.Prefetch(
        prefetch=models.Prefetch(query=[1, 23, 45, 67], using="mrl_byte", limit=1000),
        query=[0.01, 0.45, 0.67],
        using="full",
        limit=100,
    ),
    query=[
        [0.1, 0.2],
        [0.2, 0.1],
        [0.8, 0.9],
    ],
    using="colbert",
    limit=10,
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

client.query("{collection_name}", {
    prefetch: {
        prefetch: {
            query: [1, 23, 45, 67],
            using: 'mrl_byte',
            limit: 1000
        },
        query: [0.01, 0.45, 0.67],
        using: 'full',
        limit: 100,
    },
    query: [
        [0.1, 0.2],
        [0.2, 0.1],
        [0.8, 0.9],
    ],
    using: 'colbert',
    limit: 10,
});
use qdrant_client::Qdrant;
use qdrant_client::qdrant::{PrefetchQueryBuilder, Query, QueryPointsBuilder};

let client = Qdrant::from_url("https://:6334").build()?;

client.query(
    QueryPointsBuilder::new("{collection_name}")
        .add_prefetch(PrefetchQueryBuilder::default()
            .add_prefetch(PrefetchQueryBuilder::default()
                .query(Query::new_nearest(vec![1.0, 23.0, 45.0, 67.0]))
                .using("mrl_byte")
                .limit(1000u64)
            )
            .query(Query::new_nearest(vec![0.01, 0.45, 0.67]))
            .using("full")
            .limit(100u64)
        )
        .query(Query::new_nearest(vec![
            vec![0.1, 0.2],
            vec![0.2, 0.1],
            vec![0.8, 0.9],
        ]))
        .using("colbert")
        .limit(10u64)
).await?;
import static io.qdrant.client.QueryFactory.nearest;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.PrefetchQuery;
import io.qdrant.client.grpc.Points.QueryPoints;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
    .queryAsync(
        QueryPoints.newBuilder()
            .setCollectionName("{collection_name}")
            .addPrefetch(
                PrefetchQuery.newBuilder()
                    .addPrefetch(
                        PrefetchQuery.newBuilder()
                            .setQuery(nearest(1, 23, 45, 67)) // <------------- small byte vector
                            .setUsing("mrl_byte")
                            .setLimit(1000)
                            .build())
                    .setQuery(nearest(0.01f, 0.45f, 0.67f)) // <-- dense vector
                    .setUsing("full")
                    .setLimit(100)
                    .build())
            .setQuery(
                nearest(
                    new float[][] {
                      {0.1f, 0.2f}, // <─┐
                      {0.2f, 0.1f}, // < ├─ multi-vector
                      {0.8f, 0.9f}  // < ┘
                    }))
            .setUsing("colbert")
            .setLimit(10)
            .build())
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.QueryAsync(
  collectionName: "{collection_name}",
  prefetch: new List <PrefetchQuery> {
    new() {
      Prefetch = {
          new List <PrefetchQuery> {
            new() {
              Query = new float[] { 1, 23, 45, 67 }, // <------------- small byte vector
                Using = "mrl_byte",
                Limit = 1000
            },
          }
        },
        Query = new float[] {0.01f, 0.45f, 0.67f}, // <-- dense vector
        Using = "full",
        Limit = 100
    }
  },
  query: new float[][] {
    [0.1f, 0.2f], // <─┐
    [0.2f, 0.1f], // < ├─ multi-vector
    [0.8f, 0.9f]  // < ┘
  },
  usingVector: "colbert",
  limit: 10
);

注意: 多向量功能不仅对 ColBERT 有用,它还可以以其他方式使用。
例如,在电商领域,您可以使用多向量来存储同一商品的多张图片。这是分组 (group-by) 方法的一种替代方案。

稀疏向量压缩

在 1.9 版本中,我们为稀疏向量引入了 uint8 向量数据类型,以支持来自 JinaAI 和 Cohere 等公司的预量化嵌入。这次,我们为稀疏和稠密向量引入了新的数据类型,以及一种不同的存储这些向量的方式。

数据类型: 稀疏和稠密向量以前使用较大的 float32 值表示,现在可以转换为 float16。与 float32 相比,float16 向量的精度较低,这意味着向量值的数值精度稍有损失——但对于实际应用场景而言,这种差异可以忽略不计。

这些向量将占用常规向量一半的内存,这可以显著减少大型向量数据集的占用空间。由于降低了内存带宽需求并提高了缓存利用率,操作速度可能更快。这在内存受限的场景中可以带来更快的向量搜索速度。

创建集合时,需要预先指定 datatype

PUT /collections/{collection_name}
{
    "vectors": {
      "size": 1024,
      "distance": "Cosine",
      "datatype": "float16"
    }
}
from qdrant_client import QdrantClient, models

client = QdrantClient(url="https://:6333")

client.create_collection(
    "{collection_name}",
    vectors_config=models.VectorParams(
        size=1024, distance=models.Distance.COSINE, datatype=models.Datatype.FLOAT16
    ),
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

client.createCollection("{collection_name}", {
    vectors: {
        size: 1024,
        distance: "Cosine",
        datatype: "float16"
    }
});
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateCollection;
import io.qdrant.client.grpc.Collections.Datatype;
import io.qdrant.client.grpc.Collections.Distance;
import io.qdrant.client.grpc.Collections.VectorParams;
import io.qdrant.client.grpc.Collections.VectorsConfig;

QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
  .createCollectionAsync(
    CreateCollection.newBuilder()
    .setCollectionName("{collection_name}")
    .setVectorsConfig(VectorsConfig.newBuilder()
      .setParams(VectorParams.newBuilder()
        .setSize(1024)
        .setDistance(Distance.Cosine)
        .setDatatype(Datatype.Float16)
        .build())
      .build())
    .build())
  .get();
use qdrant_client::Qdrant;
use qdrant_client::qdrant::{CreateCollectionBuilder, Datatype, Distance, VectorParamsBuilder};

let client = Qdrant::from_url("https://:6334").build()?;

client
    .create_collection(
        CreateCollectionBuilder::new("{collection_name}").vectors_config(
            VectorParamsBuilder::new(1024, Distance::Cosine).datatype(Datatype::Float16),
        ),
    )
    .await?;
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.CreateCollectionAsync(
  collectionName: "{collection_name}",
  vectorsConfig: new VectorParams {
    Size = 1024,
    Distance = Distance.Cosine,
    Datatype = Datatype.Float16
  }
);

存储: 在后端,我们实现了位打包 (bit packing) 以最大限度地减少存储数据所需的位数,这对于处理机器学习和数据压缩等应用中的稀疏向量至关重要。对于大部分为零的稀疏向量,这专注于仅存储非零元素的索引和值。

您将受益于更紧凑的存储和更高的处理效率。这还可以减少数据集大小,从而在数据压缩中实现更快的处理速度和更低的存储成本。

全新的 Rust 客户端

Qdrant 的 Rust 客户端已全面重构,现在更易于使用。我们专注于构建简约的 API 接口。所有操作及其类型现在都使用构建器模式 (builder pattern),提供了一个易于扩展的接口,防止未来更新导致代码损坏。参见 Rust ColBERT 查询作为出色示例。此外,Rust 支持安全的并发执行,这对于高效处理多个同时发生的请求至关重要。

文档也得到了显著改进,组织得更合理,并提供了全面的使用示例。所有内容都链接回我们的主文档,使导航和查找所需信息变得更加容易。

访问我们的客户端操作文档。

S3 快照存储

Qdrant 的集合分片存储可以通过快照进行备份,以防数据丢失或用于数据迁移。这些快照可能非常大,维护它们所需的资源可能导致成本增加。AWS S3 及其他 S3 兼容的实现(如 min.io)是极佳的低成本替代方案,可以存储快照而不会产生高昂费用。它具有全球可靠性、可扩展性,且能抵抗数据丢失。

您可以在 config.yaml 中配置 S3 存储设置,特别是 snapshots_storage

例如,使用 AWS S3:

storage:
  snapshots_config:
    # Use 's3' to store snapshots on S3
    snapshots_storage: s3

    s3_config:
      # Bucket name
      bucket: your_bucket_here

      # Bucket region (e.g. eu-central-1)
      region: your_bucket_region_here

      # Storage access key
      # Can be specified either here or in the `AWS_ACCESS_KEY_ID` environment variable.
      access_key: your_access_key_here

      # Storage secret key
      # Can be specified either here or in the `AWS_SECRET_ACCESS_KEY` environment variable.
      secret_key: your_secret_key_here

阅读更多关于 S3 快照存储配置的信息。

这种集成实现了更便捷的快照分发。任何兼容 S3 的对象存储用户现在都可以受益于其他平台服务,例如自动化工作流和灾难恢复选项。S3 的加密和访问控制确保了存储的安全性与合规性。此外,S3 通过各种存储类别和高效的数据传输方法支持性能优化,实现了快速且有效的快照检索与管理。

问题 API (Issues API)

问题 API (Issues API) 会通知您潜在的性能问题和配置错误。这一强大的新功能允许用户(例如数据库管理员)直接在系统内高效管理和跟踪问题,确保系统运行更加平稳并更快解决故障。

您可以在右上角找到“问题”按钮。点击铃铛图标,将打开一个侧边栏以显示当前存在的问题。

issues api

功能改进

  • 预配置集合参数;量化、向量存储及复制因子 - #4299

  • 覆盖集合的全局优化器配置。允许您在单个 Qdrant 集群内分离索引和搜索的角色 - #4317

  • 针对稀疏向量的增量编码和位打包压缩,将稀疏向量的内存消耗降低了多达 75% - #4253, #4350

免费开始使用 Qdrant

开始使用