Merge pull request #2362 from qdrant/with_lookup-example

Add with_lookup example
This commit is contained in:
Dylan Couzon
2026-05-21 11:04:34 -04:00
committed by GitHub
42 changed files with 953 additions and 38 deletions
@@ -0,0 +1 @@
Populate both collections for the `with_lookup` pattern. The `documents` collection holds one point per document with payload only. Each chunk in `chunks` carries a `document_id` payload value that matches the id of a point in `documents`.
@@ -0,0 +1,56 @@
using Qdrant.Client;
using Qdrant.Client.Grpc;
public class Snippet
{
public static async Task Run()
{
var client = new QdrantClient("localhost", 6334);
await client.UpsertAsync(
collectionName: "documents",
points: new List<PointStruct>
{
new()
{
Id = 200,
Vectors = new Dictionary<string, Vector>(),
Payload =
{
["title"] = "Document A",
["text"] = "This is document A",
},
},
new()
{
Id = 201,
Vectors = new Dictionary<string, Vector>(),
Payload =
{
["title"] = "Document B",
["text"] = "This is document B",
},
},
}
);
await client.UpsertAsync(
collectionName: "chunks",
points: new List<PointStruct>
{
new()
{
Id = 0,
Vectors = new float[] { 0.1f, 0.2f, 0.3f, 0.4f },
Payload = { ["document_id"] = 200 },
},
new()
{
Id = 1,
Vectors = new float[] { 0.5f, 0.6f, 0.7f, 0.8f },
Payload = { ["document_id"] = new Value[] { 200L, 201L } },
},
}
);
}
}
@@ -0,0 +1,52 @@
```csharp
using Qdrant.Client;
using Qdrant.Client.Grpc;
var client = new QdrantClient("localhost", 6334);
await client.UpsertAsync(
collectionName: "documents",
points: new List<PointStruct>
{
new()
{
Id = 200,
Vectors = new Dictionary<string, Vector>(),
Payload =
{
["title"] = "Document A",
["text"] = "This is document A",
},
},
new()
{
Id = 201,
Vectors = new Dictionary<string, Vector>(),
Payload =
{
["title"] = "Document B",
["text"] = "This is document B",
},
},
}
);
await client.UpsertAsync(
collectionName: "chunks",
points: new List<PointStruct>
{
new()
{
Id = 0,
Vectors = new float[] { 0.1f, 0.2f, 0.3f, 0.4f },
Payload = { ["document_id"] = 200 },
},
new()
{
Id = 1,
Vectors = new float[] { 0.5f, 0.6f, 0.7f, 0.8f },
Payload = { ["document_id"] = new Value[] { 200L, 201L } },
},
}
);
```
@@ -0,0 +1,50 @@
```go
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Upsert(context.Background(), &qdrant.UpsertPoints{
CollectionName: "documents",
Points: []*qdrant.PointStruct{
{
Id: qdrant.NewIDNum(200),
Vectors: qdrant.NewVectorsMap(map[string]*qdrant.Vector{}),
Payload: qdrant.NewValueMap(map[string]any{
"title": "Document A",
"text": "This is document A",
}),
},
{
Id: qdrant.NewIDNum(201),
Vectors: qdrant.NewVectorsMap(map[string]*qdrant.Vector{}),
Payload: qdrant.NewValueMap(map[string]any{
"title": "Document B",
"text": "This is document B",
}),
},
},
})
client.Upsert(context.Background(), &qdrant.UpsertPoints{
CollectionName: "chunks",
Points: []*qdrant.PointStruct{
{
Id: qdrant.NewIDNum(0),
Vectors: qdrant.NewVectors(0.1, 0.2, 0.3, 0.4),
Payload: qdrant.NewValueMap(map[string]any{"document_id": 200}),
},
{
Id: qdrant.NewIDNum(1),
Vectors: qdrant.NewVectors(0.5, 0.6, 0.7, 0.8),
Payload: qdrant.NewValueMap(map[string]any{"document_id": []int{200, 201}}),
},
},
})
```
@@ -0,0 +1,48 @@
```java
import static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.ValueFactory.list;
import static io.qdrant.client.ValueFactory.value;
import static io.qdrant.client.VectorsFactory.namedVectors;
import static io.qdrant.client.VectorsFactory.vectors;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.PointStruct;
import java.util.List;
import java.util.Map;
QdrantClient client = new QdrantClient(
QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
client.upsertAsync(
"documents",
List.of(
PointStruct.newBuilder()
.setId(id(200))
.setVectors(namedVectors(Map.of()))
.putAllPayload(Map.of(
"title", value("Document A"),
"text", value("This is document A")))
.build(),
PointStruct.newBuilder()
.setId(id(201))
.setVectors(namedVectors(Map.of()))
.putAllPayload(Map.of(
"title", value("Document B"),
"text", value("This is document B")))
.build())).get();
client.upsertAsync(
"chunks",
List.of(
PointStruct.newBuilder()
.setId(id(0))
.setVectors(vectors(0.1f, 0.2f, 0.3f, 0.4f))
.putAllPayload(Map.of("document_id", value(200)))
.build(),
PointStruct.newBuilder()
.setId(id(1))
.setVectors(vectors(0.5f, 0.6f, 0.7f, 0.8f))
.putAllPayload(Map.of("document_id", list(List.of(value(200), value(201)))))
.build())).get();
```
@@ -0,0 +1,33 @@
```python
client.upsert(
collection_name="documents",
points=[
models.PointStruct(
id=200,
vector={},
payload={"title": "Document A", "text": "This is document A"},
),
models.PointStruct(
id=201,
vector={},
payload={"title": "Document B", "text": "This is document B"},
),
],
)
client.upsert(
collection_name="chunks",
points=[
models.PointStruct(
id=0,
vector=[0.1, 0.2, 0.3, 0.4],
payload={"document_id": 200},
),
models.PointStruct(
id=1,
vector=[0.5, 0.6, 0.7, 0.8],
payload={"document_id": [200, 201]},
),
],
)
```
@@ -0,0 +1,40 @@
```rust
use std::collections::HashMap;
use qdrant_client::Qdrant;
use qdrant_client::qdrant::{PointStruct, UpsertPointsBuilder, Vector};
client
.upsert_points(UpsertPointsBuilder::new(
"documents",
vec![
PointStruct::new(
200,
HashMap::<String, Vector>::new(),
[
("title", "Document A".into()),
("text", "This is document A".into()),
],
),
PointStruct::new(
201,
HashMap::<String, Vector>::new(),
[
("title", "Document B".into()),
("text", "This is document B".into()),
],
),
],
))
.await?;
client
.upsert_points(UpsertPointsBuilder::new(
"chunks",
vec![
PointStruct::new(0, vec![0.1, 0.2, 0.3, 0.4], [("document_id", 200.into())]),
PointStruct::new(1, vec![0.5, 0.6, 0.7, 0.8], [("document_id", vec![200, 201].into())]),
],
))
.await?;
```
@@ -0,0 +1,31 @@
```typescript
await client.upsert("documents", {
points: [
{
id: 200,
vector: {},
payload: { title: "Document A", text: "This is document A" },
},
{
id: 201,
vector: {},
payload: { title: "Document B", text: "This is document B" },
},
],
});
await client.upsert("chunks", {
points: [
{
id: 0,
vector: [0.1, 0.2, 0.3, 0.4],
payload: { document_id: 200 },
},
{
id: 1,
vector: [0.5, 0.6, 0.7, 0.8],
payload: { document_id: [200, 201] },
},
],
});
```
@@ -0,0 +1,54 @@
package snippet
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
func Main() {
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
if err != nil { panic(err) } // @hide
client.Upsert(context.Background(), &qdrant.UpsertPoints{
CollectionName: "documents",
Points: []*qdrant.PointStruct{
{
Id: qdrant.NewIDNum(200),
Vectors: qdrant.NewVectorsMap(map[string]*qdrant.Vector{}),
Payload: qdrant.NewValueMap(map[string]any{
"title": "Document A",
"text": "This is document A",
}),
},
{
Id: qdrant.NewIDNum(201),
Vectors: qdrant.NewVectorsMap(map[string]*qdrant.Vector{}),
Payload: qdrant.NewValueMap(map[string]any{
"title": "Document B",
"text": "This is document B",
}),
},
},
})
client.Upsert(context.Background(), &qdrant.UpsertPoints{
CollectionName: "chunks",
Points: []*qdrant.PointStruct{
{
Id: qdrant.NewIDNum(0),
Vectors: qdrant.NewVectors(0.1, 0.2, 0.3, 0.4),
Payload: qdrant.NewValueMap(map[string]any{"document_id": 200}),
},
{
Id: qdrant.NewIDNum(1),
Vectors: qdrant.NewVectors(0.5, 0.6, 0.7, 0.8),
Payload: qdrant.NewValueMap(map[string]any{"document_id": []int{200, 201}}),
},
},
})
}
@@ -0,0 +1,33 @@
```http
PUT /collections/documents/points
{
"points": [
{
"id": 200,
"vector": {},
"payload": {"title": "Document A", "text": "This is document A"}
},
{
"id": 201,
"vector": {},
"payload": {"title": "Document B", "text": "This is document B"}
}
]
}
PUT /collections/chunks/points
{
"points": [
{
"id": 0,
"vector": [0.1, 0.2, 0.3, 0.4],
"payload": {"document_id": 200}
},
{
"id": 1,
"vector": [0.5, 0.6, 0.7, 0.8],
"payload": {"document_id": [200, 201]}
}
]
}
```
@@ -0,0 +1,52 @@
package com.example.snippets_amalgamation;
import static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.ValueFactory.list;
import static io.qdrant.client.ValueFactory.value;
import static io.qdrant.client.VectorsFactory.namedVectors;
import static io.qdrant.client.VectorsFactory.vectors;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.PointStruct;
import java.util.List;
import java.util.Map;
public class Snippet {
public static void run() throws Exception {
QdrantClient client = new QdrantClient(
QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
client.upsertAsync(
"documents",
List.of(
PointStruct.newBuilder()
.setId(id(200))
.setVectors(namedVectors(Map.of()))
.putAllPayload(Map.of(
"title", value("Document A"),
"text", value("This is document A")))
.build(),
PointStruct.newBuilder()
.setId(id(201))
.setVectors(namedVectors(Map.of()))
.putAllPayload(Map.of(
"title", value("Document B"),
"text", value("This is document B")))
.build())).get();
client.upsertAsync(
"chunks",
List.of(
PointStruct.newBuilder()
.setId(id(0))
.setVectors(vectors(0.1f, 0.2f, 0.3f, 0.4f))
.putAllPayload(Map.of("document_id", value(200)))
.build(),
PointStruct.newBuilder()
.setId(id(1))
.setVectors(vectors(0.5f, 0.6f, 0.7f, 0.8f))
.putAllPayload(Map.of("document_id", list(List.of(value(200), value(201)))))
.build())).get();
}
}
@@ -0,0 +1,35 @@
from qdrant_client import QdrantClient, models # @hide
client = QdrantClient(url="http://localhost:6333") # @hide
client.upsert(
collection_name="documents",
points=[
models.PointStruct(
id=200,
vector={},
payload={"title": "Document A", "text": "This is document A"},
),
models.PointStruct(
id=201,
vector={},
payload={"title": "Document B", "text": "This is document B"},
),
],
)
client.upsert(
collection_name="chunks",
points=[
models.PointStruct(
id=0,
vector=[0.1, 0.2, 0.3, 0.4],
payload={"document_id": 200},
),
models.PointStruct(
id=1,
vector=[0.5, 0.6, 0.7, 0.8],
payload={"document_id": [200, 201]},
),
],
)
@@ -0,0 +1,44 @@
use std::collections::HashMap;
use qdrant_client::Qdrant;
use qdrant_client::qdrant::{PointStruct, UpsertPointsBuilder, Vector};
pub async fn main() -> anyhow::Result<()> {
let client = Qdrant::from_url("http://localhost:6334").build()?; // @hide
client
.upsert_points(UpsertPointsBuilder::new(
"documents",
vec![
PointStruct::new(
200,
HashMap::<String, Vector>::new(),
[
("title", "Document A".into()),
("text", "This is document A".into()),
],
),
PointStruct::new(
201,
HashMap::<String, Vector>::new(),
[
("title", "Document B".into()),
("text", "This is document B".into()),
],
),
],
))
.await?;
client
.upsert_points(UpsertPointsBuilder::new(
"chunks",
vec![
PointStruct::new(0, vec![0.1, 0.2, 0.3, 0.4], [("document_id", 200.into())]),
PointStruct::new(1, vec![0.5, 0.6, 0.7, 0.8], [("document_id", vec![200, 201].into())]),
],
))
.await?;
Ok(())
}
@@ -0,0 +1,33 @@
import { QdrantClient } from "@qdrant/js-client-rest"; // @hide
const client = new QdrantClient({ host: "localhost", port: 6333 }); // @hide
await client.upsert("documents", {
points: [
{
id: 200,
vector: {},
payload: { title: "Document A", text: "This is document A" },
},
{
id: 201,
vector: {},
payload: { title: "Document B", text: "This is document B" },
},
],
});
await client.upsert("chunks", {
points: [
{
id: 0,
vector: [0.1, 0.2, 0.3, 0.4],
payload: { document_id: 200 },
},
{
id: 1,
vector: [0.5, 0.6, 0.7, 0.8],
payload: { document_id: [200, 201] },
},
],
});
@@ -0,0 +1 @@
Create the two collections used by the `with_lookup` pattern. The `chunks` collection holds one point per chunk with its own vector. The `documents` collection holds one point per document and carries only payload, no vectors. A payload index on `document_id` is required for `group_by` to work on that field.
@@ -0,0 +1,27 @@
using Qdrant.Client;
using Qdrant.Client.Grpc;
public class Snippet
{
public static async Task Run()
{
var client = new QdrantClient("localhost", 6334);
await client.CreateCollectionAsync(
collectionName: "chunks",
vectorsConfig: new VectorParams { Size = 4, Distance = Distance.Cosine }
);
await client.CreatePayloadIndexAsync(
collectionName: "chunks",
fieldName: "document_id",
schemaType: PayloadSchemaType.Integer
);
// No vectors, payload only.
await client.CreateCollectionAsync(
collectionName: "documents",
vectorsConfig: new VectorParamsMap()
);
}
}
@@ -0,0 +1,23 @@
```csharp
using Qdrant.Client;
using Qdrant.Client.Grpc;
var client = new QdrantClient("localhost", 6334);
await client.CreateCollectionAsync(
collectionName: "chunks",
vectorsConfig: new VectorParams { Size = 4, Distance = Distance.Cosine }
);
await client.CreatePayloadIndexAsync(
collectionName: "chunks",
fieldName: "document_id",
schemaType: PayloadSchemaType.Integer
);
// No vectors, payload only.
await client.CreateCollectionAsync(
collectionName: "documents",
vectorsConfig: new VectorParamsMap()
);
```
@@ -0,0 +1,34 @@
```go
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.CreateCollection(context.Background(), &qdrant.CreateCollection{
CollectionName: "chunks",
VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{
Size: 4,
Distance: qdrant.Distance_Cosine,
}),
})
client.CreateFieldIndex(context.Background(), &qdrant.CreateFieldIndexCollection{
CollectionName: "chunks",
FieldName: "document_id",
FieldType: qdrant.FieldType_FieldTypeInteger.Enum(),
})
// No vectors, payload only.
client.CreateCollection(context.Background(), &qdrant.CreateCollection{
CollectionName: "documents",
VectorsConfig: qdrant.NewVectorsConfigMap(
map[string]*qdrant.VectorParams{},
),
})
```
@@ -0,0 +1,34 @@
```java
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateCollection;
import io.qdrant.client.grpc.Collections.Distance;
import io.qdrant.client.grpc.Collections.PayloadSchemaType;
import io.qdrant.client.grpc.Collections.VectorParams;
import io.qdrant.client.grpc.Collections.VectorParamsMap;
import io.qdrant.client.grpc.Collections.VectorsConfig;
QdrantClient client = new QdrantClient(
QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
client.createCollectionAsync("chunks",
VectorParams.newBuilder().setDistance(Distance.Cosine).setSize(4).build()).get();
client.createPayloadIndexAsync(
"chunks",
"document_id",
PayloadSchemaType.Integer,
null,
true,
null,
null).get();
// No vectors, payload only.
client.createCollectionAsync(
CreateCollection.newBuilder()
.setCollectionName("documents")
.setVectorsConfig(VectorsConfig.newBuilder()
.setParamsMap(VectorParamsMap.newBuilder().build())
.build())
.build()).get();
```
@@ -0,0 +1,17 @@
```python
client.create_collection(
collection_name="chunks",
vectors_config=models.VectorParams(size=4, distance=models.Distance.COSINE),
)
client.create_payload_index(
collection_name="chunks",
field_name="document_id",
field_schema=models.PayloadSchemaType.INTEGER,
)
client.create_collection(
collection_name="documents",
vectors_config={}, # no vectors, payload only
)
```
@@ -0,0 +1,29 @@
```rust
use qdrant_client::Qdrant;
use qdrant_client::qdrant::{
CreateCollectionBuilder, CreateFieldIndexCollectionBuilder, Distance, FieldType,
VectorParamsBuilder, VectorsConfigBuilder,
};
client
.create_collection(
CreateCollectionBuilder::new("chunks")
.vectors_config(VectorParamsBuilder::new(4, Distance::Cosine)),
)
.await?;
client
.create_field_index(
CreateFieldIndexCollectionBuilder::new("chunks", "document_id", FieldType::Integer)
.wait(true),
)
.await?;
// No vectors, payload only.
client
.create_collection(
CreateCollectionBuilder::new("documents")
.vectors_config(VectorsConfigBuilder::default()),
)
.await?;
```
@@ -0,0 +1,14 @@
```typescript
await client.createCollection("chunks", {
vectors: { size: 4, distance: "Cosine" },
});
await client.createPayloadIndex("chunks", {
field_name: "document_id",
field_schema: "integer",
});
await client.createCollection("documents", {
vectors: {}, // no vectors, payload only
});
```
@@ -0,0 +1,38 @@
package snippet
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
func Main() {
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
if err != nil { panic(err) } // @hide
client.CreateCollection(context.Background(), &qdrant.CreateCollection{
CollectionName: "chunks",
VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{
Size: 4,
Distance: qdrant.Distance_Cosine,
}),
})
client.CreateFieldIndex(context.Background(), &qdrant.CreateFieldIndexCollection{
CollectionName: "chunks",
FieldName: "document_id",
FieldType: qdrant.FieldType_FieldTypeInteger.Enum(),
})
// No vectors, payload only.
client.CreateCollection(context.Background(), &qdrant.CreateCollection{
CollectionName: "documents",
VectorsConfig: qdrant.NewVectorsConfigMap(
map[string]*qdrant.VectorParams{},
),
})
}
@@ -0,0 +1,20 @@
```http
PUT /collections/chunks
{
"vectors": {
"size": 4,
"distance": "Cosine"
}
}
PUT /collections/chunks/index
{
"field_name": "document_id",
"field_schema": "integer"
}
PUT /collections/documents
{
"vectors": {}
}
```
@@ -0,0 +1,38 @@
package com.example.snippets_amalgamation;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateCollection;
import io.qdrant.client.grpc.Collections.Distance;
import io.qdrant.client.grpc.Collections.PayloadSchemaType;
import io.qdrant.client.grpc.Collections.VectorParams;
import io.qdrant.client.grpc.Collections.VectorParamsMap;
import io.qdrant.client.grpc.Collections.VectorsConfig;
public class Snippet {
public static void run() throws Exception {
QdrantClient client = new QdrantClient(
QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
client.createCollectionAsync("chunks",
VectorParams.newBuilder().setDistance(Distance.Cosine).setSize(4).build()).get();
client.createPayloadIndexAsync(
"chunks",
"document_id",
PayloadSchemaType.Integer,
null,
true,
null,
null).get();
// No vectors, payload only.
client.createCollectionAsync(
CreateCollection.newBuilder()
.setCollectionName("documents")
.setVectorsConfig(VectorsConfig.newBuilder()
.setParamsMap(VectorParamsMap.newBuilder().build())
.build())
.build()).get();
}
}
@@ -0,0 +1,19 @@
from qdrant_client import QdrantClient, models # @hide
client = QdrantClient(url="http://localhost:6333") # @hide
client.create_collection(
collection_name="chunks",
vectors_config=models.VectorParams(size=4, distance=models.Distance.COSINE),
)
client.create_payload_index(
collection_name="chunks",
field_name="document_id",
field_schema=models.PayloadSchemaType.INTEGER,
)
client.create_collection(
collection_name="documents",
vectors_config={}, # no vectors, payload only
)
@@ -0,0 +1,33 @@
use qdrant_client::Qdrant;
use qdrant_client::qdrant::{
CreateCollectionBuilder, CreateFieldIndexCollectionBuilder, Distance, FieldType,
VectorParamsBuilder, VectorsConfigBuilder,
};
pub async fn main() -> anyhow::Result<()> {
let client = Qdrant::from_url("http://localhost:6334").build()?; // @hide
client
.create_collection(
CreateCollectionBuilder::new("chunks")
.vectors_config(VectorParamsBuilder::new(4, Distance::Cosine)),
)
.await?;
client
.create_field_index(
CreateFieldIndexCollectionBuilder::new("chunks", "document_id", FieldType::Integer)
.wait(true),
)
.await?;
// No vectors, payload only.
client
.create_collection(
CreateCollectionBuilder::new("documents")
.vectors_config(VectorsConfigBuilder::default()),
)
.await?;
Ok(())
}
@@ -0,0 +1,16 @@
import { QdrantClient } from "@qdrant/js-client-rest"; // @hide
const client = new QdrantClient({ host: "localhost", port: 6333 }); // @hide
await client.createCollection("chunks", {
vectors: { size: 4, distance: "Cosine" },
});
await client.createPayloadIndex("chunks", {
field_name: "document_id",
field_schema: "integer",
});
await client.createCollection("documents", {
vectors: {}, // no vectors, payload only
});
@@ -7,10 +7,10 @@ public class Snippet
{
var client = new QdrantClient("localhost", 6334);
await client.SearchGroupsAsync(
collectionName: "{collection_name}",
vector: new float[] { 0.2f, 0.1f, 0.9f, 0.7f},
await client.QueryGroupsAsync(
collectionName: "chunks",
groupBy: "document_id",
query: new float[] { 0.2f, 0.1f, 0.9f, 0.7f },
limit: 2,
groupSize: 2,
withLookup: new WithLookup
@@ -4,10 +4,10 @@ using Qdrant.Client.Grpc;
var client = new QdrantClient("localhost", 6334);
await client.SearchGroupsAsync(
collectionName: "{collection_name}",
vector: new float[] { 0.2f, 0.1f, 0.9f, 0.7f},
await client.QueryGroupsAsync(
collectionName: "chunks",
groupBy: "document_id",
query: new float[] { 0.2f, 0.1f, 0.9f, 0.7f },
limit: 2,
groupSize: 2,
withLookup: new WithLookup
@@ -11,9 +11,10 @@ client, err := qdrant.NewClient(&qdrant.Config{
})
client.QueryGroups(context.Background(), &qdrant.QueryPointGroups{
CollectionName: "{collection_name}",
CollectionName: "chunks",
Query: qdrant.NewQuery(0.2, 0.1, 0.9, 0.7),
GroupBy: "document_id",
Limit: qdrant.PtrOf(uint64(2)),
GroupSize: qdrant.PtrOf(uint64(2)),
WithLookup: &qdrant.WithLookup{
Collection: "documents",
@@ -9,7 +9,7 @@ import java.util.List;
client.queryGroupsAsync(
QueryPointGroups.newBuilder()
.setCollectionName("{collection_name}")
.setCollectionName("chunks")
.setQuery(nearest(0.2f, 0.1f, 0.9f, 0.7f))
.setGroupBy("document_id")
.setLimit(2)
@@ -2,7 +2,7 @@
client.query_points_groups(
collection_name="chunks",
# Same as in the regular search() API
query=[1.1],
query=[0.2, 0.1, 0.9, 0.7],
# Grouping parameters
group_by="document_id", # Path of the field to group by
limit=2, # Max amount of groups
@@ -15,7 +15,7 @@ client.query_points_groups(
# of the looked up point, True by default
with_payload=["title", "text"],
# Options for specifying what to bring from the vector(s)
# of the looked up point, True by default
# of the looked up point, False by default
with_vectors=False,
),
)
@@ -3,10 +3,10 @@ use qdrant_client::qdrant::{with_payload_selector::SelectorOptions, QueryPointGr
client
.query_groups(
QueryPointGroupsBuilder::new("{collection_name}", "document_id")
QueryPointGroupsBuilder::new("chunks", "document_id")
.query(vec![0.2, 0.1, 0.9, 0.7])
.limit(2u64)
.limit(2u64)
.group_size(2u64)
.with_lookup(
WithLookupBuilder::new("documents")
.with_payload(SelectorOptions::Include(
@@ -1,6 +1,6 @@
```typescript
client.queryGroups("{collection_name}", {
query: [1.1],
client.queryGroups("chunks", {
query: [0.2, 0.1, 0.9, 0.7],
group_by: "document_id",
limit: 2,
group_size: 2,
@@ -15,9 +15,10 @@ func Main() {
if err != nil { panic(err) } // @hide
client.QueryGroups(context.Background(), &qdrant.QueryPointGroups{
CollectionName: "{collection_name}",
CollectionName: "chunks",
Query: qdrant.NewQuery(0.2, 0.1, 0.9, 0.7),
GroupBy: "document_id",
Limit: qdrant.PtrOf(uint64(2)),
GroupSize: qdrant.PtrOf(uint64(2)),
WithLookup: &qdrant.WithLookup{
Collection: "documents",
@@ -2,7 +2,7 @@
POST /collections/chunks/points/query/groups
{
// Same as in the regular query API
"query": [1.1],
"query": [0.2, 0.1, 0.9, 0.7],
// Grouping parameters
"group_by": "document_id",
@@ -19,7 +19,7 @@ POST /collections/chunks/points/query/groups
"with_payload": ["title", "text"],
// Options for specifying what to bring from the vector(s)
// of the looked up point, true by default
// of the looked up point, false by default
"with_vectors": false
}
}
@@ -17,7 +17,7 @@ public class Snippet {
client.queryGroupsAsync(
QueryPointGroups.newBuilder()
.setCollectionName("{collection_name}")
.setCollectionName("chunks")
.setQuery(nearest(0.2f, 0.1f, 0.9f, 0.7f))
.setGroupBy("document_id")
.setLimit(2)
@@ -5,7 +5,7 @@ client = QdrantClient(url="http://localhost:6333") # @hide
client.query_points_groups(
collection_name="chunks",
# Same as in the regular search() API
query=[1.1],
query=[0.2, 0.1, 0.9, 0.7],
# Grouping parameters
group_by="document_id", # Path of the field to group by
limit=2, # Max amount of groups
@@ -18,7 +18,7 @@ client.query_points_groups(
# of the looked up point, True by default
with_payload=["title", "text"],
# Options for specifying what to bring from the vector(s)
# of the looked up point, True by default
# of the looked up point, False by default
with_vectors=False,
),
)
@@ -5,10 +5,10 @@ pub async fn main() -> anyhow::Result<()> {
client
.query_groups(
QueryPointGroupsBuilder::new("{collection_name}", "document_id")
QueryPointGroupsBuilder::new("chunks", "document_id")
.query(vec![0.2, 0.1, 0.9, 0.7])
.limit(2u64)
.limit(2u64)
.group_size(2u64)
.with_lookup(
WithLookupBuilder::new("documents")
.with_payload(SelectorOptions::Include(
@@ -2,8 +2,8 @@ import { QdrantClient } from "@qdrant/js-client-rest"; // @hide
const client = new QdrantClient({ host: "localhost", port: 6333 }); // @hide
client.queryGroups("{collection_name}", {
query: [1.1],
client.queryGroups("chunks", {
query: [0.2, 0.1, 0.9, 0.7],
group_by: "document_id",
limit: 2,
group_size: 2,
@@ -404,38 +404,48 @@ If the `group_by` field of a point is an array (e.g. `"document_id": ["a", "b"]`
### Lookup in groups
Having multiple points for parts of the same item often introduces redundancy in the stored data. Which may be fine if the information shared by the points is small, but it can become a problem if the payload is large, because it multiplies the storage space needed to store the points by a factor of the amount of points we have per group.
When the points in a group share large fields like titles, abstracts, or full document vectors, copying that data onto every point inflates storage and forces you to rewrite every chunk whenever a shared field changes.
One way of optimizing storage when using groups is to store the information shared by the points with the same group id in a single point in another collection. Then, when using the [**groups** API](#grouping-api), add the `with_lookup` parameter to bring the information from those points into each group.
`with_lookup` solves this. Store the shared data once in a separate collection, then attach it to each group at query time using the [groups API](#grouping-api).
![Group id matches point id](/docs/lookup_id_linking.png)
<aside role="status">Store only document-level metadata (e.g., titles, abstracts) in the lookup collection, not chunks or duplicated data.</aside>
This has the extra benefit of having a single point to update when the information shared by the points in a group changes.
Set up the two collections. `chunks` holds one point per chunk with its own vector. `documents` holds one point per document with payload only. The payload index on `document_id` lets `group_by` work on that field.
For example, if you have a collection of documents, you may want to chunk them and store the points for the chunks in a separate collection, making sure that you store the point id from the document it belongs in the payload of the chunk point.
{{< code-snippet path="/documentation/headless/snippets/query-groups/with-lookup-setup/" >}}
In this case, to bring the information from the documents into the chunks grouped by the document id, you can use the `with_lookup` parameter:
Ingest both collections, with `documents` populated before any query that uses `with_lookup`.
{{< code-snippet path="/documentation/headless/snippets/query-groups/with-lookup-ingest/" >}}
The lookup is a plain join by point id, not a vector search:
- The `documents` collection must already contain a point whose id matches the `group_by` value from each chunk.
- The id types must match. A string `group_by` value against integer point ids returns an empty `lookup` field with no error.
- Group ids without a matching point in `documents` get an empty `lookup` field.
Query the `chunks` collection with `group_by="document_id"` and `with_lookup` pointing at the `documents` collection:
{{< code-snippet path="/documentation/headless/snippets/query-groups/with-lookup/" >}}
For the `with_lookup` parameter, you can also use the shorthand `with_lookup="documents"` to bring the whole payload and vector(s) without explicitly specifying it.
You can also pass `with_lookup="documents"` as a shorthand. It uses the server defaults (`with_payload=True`, `with_vectors=False`), so the documents' vectors are not returned. Use the explicit `WithLookup(...)` form when you need those vectors back.
The looked up result will show up under `lookup` in each group.
The looked-up result appears under `lookup` in each group. Below, chunk id 1 shows up in both groups because its `document_id` payload is an array (`[200, 201]`), placing the chunk into every matching group.
```json
{
"result": {
"groups": [
{
"id": 1,
"id": 200,
"hits": [
{ "id": 0, "score": 0.91 },
{ "id": 1, "score": 0.85 }
],
"lookup": {
"id": 1,
"id": 200,
"payload": {
"title": "Document A",
"text": "This is document A"
@@ -443,12 +453,12 @@ The looked up result will show up under `lookup` in each group.
}
},
{
"id": 2,
"id": 201,
"hits": [
{ "id": 1, "score": 0.85 }
],
"lookup": {
"id": 2,
"id": 201,
"payload": {
"title": "Document B",
"text": "This is document B"
@@ -462,9 +472,7 @@ The looked up result will show up under `lookup` in each group.
}
```
Since the lookup is done by matching directly with the point id, the lookup collection must be pre-populated with points where the `id` matches the `group_by` value (e.g., document_id) from your primary collection.
Any group id that is not an existing (and valid) point id in the lookup collection will be ignored, and the `lookup` field will be empty.
For a collection of 20 000 documents with around 24 chunks each, duplicating ~3 KB of document-level data on every chunk adds up to ~1.4 GB. Storing those fields once per document in the `documents` collection brings that down to ~60 MB. The split pays off when the shared fields are large or change often, since updating one point in `documents` shows up under every chunk's group at query time.
## Random Sampling