mirror of
https://github.com/qdrant/landing_page.git
synced 2026-10-01 00:48:32 +02:00
Update Hybrid Search with Reranking tutorial (#2274)
* Update for Cloud Inference and data ingestion * Fix link * Review feedback * Make code snippets testable * Add C# code snippets * Add Go code snippets * Add Java code snippets * Add Rust code snippets * Add TS code snippets * Move CSV streaming/parsing to separate function
This commit is contained in:
+197
@@ -0,0 +1,197 @@
|
||||
using System.Net.Http;
|
||||
using Microsoft.VisualBasic.FileIO;
|
||||
using Qdrant.Client;
|
||||
using Qdrant.Client.Grpc;
|
||||
|
||||
public class Snippet
|
||||
{
|
||||
public static async Task Run()
|
||||
{
|
||||
// @hide-start
|
||||
string QDRANT_URL = "xyz-example.eu-central.aws.cloud.qdrant.io";
|
||||
string QDRANT_API_KEY = "<your-api-key>";
|
||||
// @hide-end
|
||||
// @block-start client-connection
|
||||
var client = new QdrantClient(
|
||||
host: QDRANT_URL,
|
||||
https: true,
|
||||
apiKey: QDRANT_API_KEY
|
||||
);
|
||||
// @block-end client-connection
|
||||
|
||||
// @block-start define-models
|
||||
string denseEmbeddingModel = "sentence-transformers/all-MiniLM-L6-v2";
|
||||
string sparseEmbeddingModel = "qdrant/bm25";
|
||||
string lateInteractionEmbeddingModel = "answerdotai/answerai-colbert-small-v1";
|
||||
// @block-end define-models
|
||||
|
||||
// @block-start create-collection
|
||||
string collectionName = "hybrid-search";
|
||||
|
||||
if (await client.CollectionExistsAsync(collectionName))
|
||||
await client.DeleteCollectionAsync(collectionName);
|
||||
|
||||
await client.CreateCollectionAsync(
|
||||
collectionName: collectionName,
|
||||
vectorsConfig: new VectorParamsMap
|
||||
{
|
||||
Map =
|
||||
{
|
||||
["dense"] = new VectorParams
|
||||
{
|
||||
Size = 384,
|
||||
Distance = Distance.Cosine,
|
||||
},
|
||||
["multi"] = new VectorParams
|
||||
{
|
||||
Size = 96,
|
||||
Distance = Distance.Cosine,
|
||||
MultivectorConfig = new() { Comparator = MultiVectorComparator.MaxSim },
|
||||
HnswConfig = new HnswConfigDiff { M = 0 }, // Disable HNSW for reranking
|
||||
},
|
||||
}
|
||||
},
|
||||
sparseVectorsConfig: new SparseVectorConfig
|
||||
{
|
||||
Map =
|
||||
{
|
||||
["sparse"] = new SparseVectorParams { Modifier = Modifier.Idf }
|
||||
}
|
||||
}
|
||||
);
|
||||
// @block-end create-collection
|
||||
|
||||
// @block-start parse-csv
|
||||
async IAsyncEnumerable<(string title, string author, string description)> ParseCsv(string url)
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
using var stream = await httpClient.GetStreamAsync(url);
|
||||
using var parser = new TextFieldParser(new StreamReader(stream));
|
||||
parser.TextFieldType = Microsoft.VisualBasic.FileIO.FieldType.Delimited;
|
||||
parser.SetDelimiters(",");
|
||||
string[]? headers = parser.ReadFields();
|
||||
int titleIdx = Array.IndexOf(headers!, "Title");
|
||||
int authorIdx = Array.IndexOf(headers!, "Author");
|
||||
int descriptionIdx = Array.IndexOf(headers!, "Description");
|
||||
while (!parser.EndOfData)
|
||||
{
|
||||
var fields = parser.ReadFields()!;
|
||||
yield return (fields[titleIdx], fields[authorIdx], fields[descriptionIdx]);
|
||||
}
|
||||
}
|
||||
// @block-end parse-csv
|
||||
|
||||
// @block-start ingest-data
|
||||
string csvUrl = "https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv";
|
||||
|
||||
int batchSize = 25;
|
||||
ulong idx = 0;
|
||||
var buffer = new List<PointStruct>();
|
||||
|
||||
await foreach (var (title, author, description) in ParseCsv(csvUrl))
|
||||
{
|
||||
buffer.Add(new PointStruct
|
||||
{
|
||||
Id = idx++,
|
||||
Vectors = new Dictionary<string, Vector>
|
||||
{
|
||||
["dense"] = new Document { Text = description, Model = denseEmbeddingModel },
|
||||
["sparse"] = new Document { Text = description, Model = sparseEmbeddingModel },
|
||||
["multi"] = new Document { Text = description, Model = lateInteractionEmbeddingModel },
|
||||
},
|
||||
Payload = { ["title"] = title, ["author"] = author, ["description"] = description }
|
||||
});
|
||||
|
||||
if (buffer.Count >= batchSize)
|
||||
{
|
||||
await client.UpsertAsync(collectionName: collectionName, points: buffer);
|
||||
buffer.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.Count > 0)
|
||||
await client.UpsertAsync(collectionName: collectionName, points: buffer);
|
||||
// @block-end ingest-data
|
||||
|
||||
// @block-start dense-retrieval
|
||||
string query = "time travel";
|
||||
|
||||
var results = await client.QueryAsync(
|
||||
collectionName: collectionName,
|
||||
query: new Document { Text = query, Model = denseEmbeddingModel },
|
||||
usingVector: "dense",
|
||||
limit: 10
|
||||
);
|
||||
|
||||
foreach (var result in results)
|
||||
Console.WriteLine(result);
|
||||
// @block-end dense-retrieval
|
||||
|
||||
// @block-start sparse-retrieval
|
||||
results = await client.QueryAsync(
|
||||
collectionName: collectionName,
|
||||
query: new Document { Text = query, Model = sparseEmbeddingModel },
|
||||
usingVector: "sparse",
|
||||
limit: 10
|
||||
);
|
||||
|
||||
foreach (var result in results)
|
||||
Console.WriteLine(result);
|
||||
// @block-end sparse-retrieval
|
||||
|
||||
// @block-start hybrid-search
|
||||
results = await client.QueryAsync(
|
||||
collectionName: collectionName,
|
||||
prefetch: new List<PrefetchQuery>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Query = new Document { Text = query, Model = denseEmbeddingModel },
|
||||
Using = "dense",
|
||||
Limit = 20,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Query = new Document { Text = query, Model = sparseEmbeddingModel },
|
||||
Using = "sparse",
|
||||
Limit = 20,
|
||||
},
|
||||
},
|
||||
query: Fusion.Rrf,
|
||||
payloadSelector: true,
|
||||
limit: 10
|
||||
);
|
||||
|
||||
foreach (var result in results)
|
||||
Console.WriteLine(result);
|
||||
// @block-end hybrid-search
|
||||
|
||||
// @block-start rerank
|
||||
results = await client.QueryAsync(
|
||||
collectionName: collectionName,
|
||||
prefetch: new List<PrefetchQuery>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Query = new Document { Text = query, Model = denseEmbeddingModel },
|
||||
Using = "dense",
|
||||
Limit = 20,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Query = new Document { Text = query, Model = sparseEmbeddingModel },
|
||||
Using = "sparse",
|
||||
Limit = 20,
|
||||
},
|
||||
},
|
||||
query: new Document { Text = query, Model = lateInteractionEmbeddingModel },
|
||||
usingVector: "multi",
|
||||
payloadSelector: true,
|
||||
limit: 10
|
||||
);
|
||||
|
||||
foreach (var result in results)
|
||||
Console.WriteLine(result);
|
||||
// @block-end rerank
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
```csharp
|
||||
var client = new QdrantClient(
|
||||
host: QDRANT_URL,
|
||||
https: true,
|
||||
apiKey: QDRANT_API_KEY
|
||||
);
|
||||
```
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
```go
|
||||
client, err := qdrant.NewClient(&qdrant.Config{
|
||||
Host: QDRANT_URL,
|
||||
APIKey: QDRANT_API_KEY,
|
||||
UseTLS: true,
|
||||
})
|
||||
```
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
```java
|
||||
QdrantClient client =
|
||||
new QdrantClient(
|
||||
QdrantGrpcClient.newBuilder(QDRANT_URL, 6334, true)
|
||||
.withApiKey(QDRANT_API_KEY)
|
||||
.build());
|
||||
```
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
```python
|
||||
from qdrant_client import QdrantClient
|
||||
|
||||
client = QdrantClient(
|
||||
url="https://xyz-example.eu-central.aws.cloud.qdrant.io:6333",
|
||||
api_key="<your-api-key>",
|
||||
cloud_inference=True,
|
||||
)
|
||||
```
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
```rust
|
||||
let client = Qdrant::from_url(qdrant_url)
|
||||
.api_key(qdrant_api_key)
|
||||
.build()?;
|
||||
```
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
```typescript
|
||||
const client = new QdrantClient({
|
||||
url: QDRANT_URL,
|
||||
apiKey: QDRANT_API_KEY,
|
||||
});
|
||||
```
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
```csharp
|
||||
string collectionName = "hybrid-search";
|
||||
|
||||
if (await client.CollectionExistsAsync(collectionName))
|
||||
await client.DeleteCollectionAsync(collectionName);
|
||||
|
||||
await client.CreateCollectionAsync(
|
||||
collectionName: collectionName,
|
||||
vectorsConfig: new VectorParamsMap
|
||||
{
|
||||
Map =
|
||||
{
|
||||
["dense"] = new VectorParams
|
||||
{
|
||||
Size = 384,
|
||||
Distance = Distance.Cosine,
|
||||
},
|
||||
["multi"] = new VectorParams
|
||||
{
|
||||
Size = 96,
|
||||
Distance = Distance.Cosine,
|
||||
MultivectorConfig = new() { Comparator = MultiVectorComparator.MaxSim },
|
||||
HnswConfig = new HnswConfigDiff { M = 0 }, // Disable HNSW for reranking
|
||||
},
|
||||
}
|
||||
},
|
||||
sparseVectorsConfig: new SparseVectorConfig
|
||||
{
|
||||
Map =
|
||||
{
|
||||
["sparse"] = new SparseVectorParams { Modifier = Modifier.Idf }
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
```go
|
||||
collectionName := "hybrid-search"
|
||||
|
||||
exists, err := client.CollectionExists(context.Background(), collectionName)
|
||||
if exists {
|
||||
client.DeleteCollection(context.Background(), collectionName)
|
||||
}
|
||||
|
||||
client.CreateCollection(context.Background(), &qdrant.CreateCollection{
|
||||
CollectionName: collectionName,
|
||||
VectorsConfig: qdrant.NewVectorsConfigMap(
|
||||
map[string]*qdrant.VectorParams{
|
||||
"dense": {
|
||||
Size: 384,
|
||||
Distance: qdrant.Distance_Cosine,
|
||||
},
|
||||
"multi": {
|
||||
Size: 96,
|
||||
Distance: qdrant.Distance_Cosine,
|
||||
MultivectorConfig: &qdrant.MultiVectorConfig{
|
||||
Comparator: qdrant.MultiVectorComparator_MaxSim,
|
||||
},
|
||||
HnswConfig: &qdrant.HnswConfigDiff{M: qdrant.PtrOf(uint64(0))}, // Disable HNSW for reranking
|
||||
},
|
||||
},
|
||||
),
|
||||
SparseVectorsConfig: qdrant.NewSparseVectorsConfig(
|
||||
map[string]*qdrant.SparseVectorParams{
|
||||
"sparse": {Modifier: qdrant.Modifier_Idf.Enum()},
|
||||
},
|
||||
),
|
||||
})
|
||||
```
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
```java
|
||||
String collectionName = "hybrid-search";
|
||||
|
||||
if (client.collectionExistsAsync(collectionName).get()) {
|
||||
client.deleteCollectionAsync(collectionName).get();
|
||||
}
|
||||
|
||||
client.createCollectionAsync(
|
||||
CreateCollection.newBuilder()
|
||||
.setCollectionName(collectionName)
|
||||
.setVectorsConfig(
|
||||
VectorsConfig.newBuilder()
|
||||
.setParamsMap(
|
||||
VectorParamsMap.newBuilder()
|
||||
.putMap(
|
||||
"dense",
|
||||
VectorParams.newBuilder()
|
||||
.setSize(384)
|
||||
.setDistance(Distance.Cosine)
|
||||
.build())
|
||||
.putMap(
|
||||
"multi",
|
||||
VectorParams.newBuilder()
|
||||
.setSize(96)
|
||||
.setDistance(Distance.Cosine)
|
||||
.setMultivectorConfig(
|
||||
MultiVectorConfig.newBuilder()
|
||||
.setComparator(MultiVectorComparator.MaxSim)
|
||||
.build())
|
||||
.setHnswConfig(
|
||||
HnswConfigDiff.newBuilder()
|
||||
.setM(0) // Disable HNSW for reranking
|
||||
.build())
|
||||
.build())
|
||||
.build()))
|
||||
.setSparseVectorsConfig(
|
||||
SparseVectorConfig.newBuilder()
|
||||
.putMap(
|
||||
"sparse",
|
||||
SparseVectorParams.newBuilder()
|
||||
.setModifier(Modifier.Idf)
|
||||
.build())
|
||||
.build())
|
||||
.build()
|
||||
).get();
|
||||
```
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
```python
|
||||
from qdrant_client.models import Distance, VectorParams, models
|
||||
|
||||
collection_name = "hybrid-search"
|
||||
|
||||
if client.collection_exists(collection_name=collection_name):
|
||||
client.delete_collection(collection_name=collection_name)
|
||||
|
||||
client.create_collection(
|
||||
collection_name,
|
||||
vectors_config={
|
||||
"dense": models.VectorParams(
|
||||
size=384,
|
||||
distance=models.Distance.COSINE,
|
||||
),
|
||||
"multi": models.VectorParams(
|
||||
size=96,
|
||||
distance=models.Distance.COSINE,
|
||||
multivector_config=models.MultiVectorConfig(
|
||||
comparator=models.MultiVectorComparator.MAX_SIM,
|
||||
),
|
||||
hnsw_config=models.HnswConfigDiff(m=0) # Disable HNSW for reranking
|
||||
),
|
||||
},
|
||||
sparse_vectors_config={
|
||||
"sparse": models.SparseVectorParams(modifier=models.Modifier.IDF)
|
||||
}
|
||||
)
|
||||
```
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
```rust
|
||||
let collection_name = "hybrid-search";
|
||||
|
||||
if client.collection_exists(collection_name).await? {
|
||||
client.delete_collection(collection_name).await?;
|
||||
}
|
||||
|
||||
let mut vectors = VectorsConfigBuilder::default();
|
||||
vectors.add_named_vector_params(
|
||||
"dense",
|
||||
VectorParamsBuilder::new(384, Distance::Cosine),
|
||||
);
|
||||
vectors.add_named_vector_params(
|
||||
"multi",
|
||||
VectorParamsBuilder::new(96, Distance::Cosine)
|
||||
.multivector_config(MultiVectorConfigBuilder::new(MultiVectorComparator::MaxSim))
|
||||
.hnsw_config(HnswConfigDiffBuilder::default().m(0)), // Disable HNSW for reranking
|
||||
);
|
||||
|
||||
let mut sparse = SparseVectorsConfigBuilder::default();
|
||||
sparse.add_named_vector_params(
|
||||
"sparse",
|
||||
SparseVectorParamsBuilder::default().modifier(Modifier::Idf),
|
||||
);
|
||||
|
||||
client
|
||||
.create_collection(
|
||||
CreateCollectionBuilder::new(collection_name)
|
||||
.vectors_config(vectors)
|
||||
.sparse_vectors_config(sparse),
|
||||
)
|
||||
.await?;
|
||||
```
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
```typescript
|
||||
const collectionName = "hybrid-search";
|
||||
|
||||
if (await client.collectionExists(collectionName)) {
|
||||
await client.deleteCollection(collectionName);
|
||||
}
|
||||
|
||||
await client.createCollection(collectionName, {
|
||||
vectors: {
|
||||
dense: {
|
||||
size: 384,
|
||||
distance: "Cosine",
|
||||
},
|
||||
multi: {
|
||||
size: 96,
|
||||
distance: "Cosine",
|
||||
multivector_config: { comparator: "max_sim" },
|
||||
hnsw_config: { m: 0 }, // Disable HNSW for reranking
|
||||
},
|
||||
},
|
||||
sparse_vectors: {
|
||||
sparse: { modifier: "idf" },
|
||||
},
|
||||
});
|
||||
```
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
```csharp
|
||||
using System.Net.Http;
|
||||
using Microsoft.VisualBasic.FileIO;
|
||||
using Qdrant.Client;
|
||||
using Qdrant.Client.Grpc;
|
||||
|
||||
var client = new QdrantClient(
|
||||
host: QDRANT_URL,
|
||||
https: true,
|
||||
apiKey: QDRANT_API_KEY
|
||||
);
|
||||
|
||||
string denseEmbeddingModel = "sentence-transformers/all-MiniLM-L6-v2";
|
||||
string sparseEmbeddingModel = "qdrant/bm25";
|
||||
string lateInteractionEmbeddingModel = "answerdotai/answerai-colbert-small-v1";
|
||||
|
||||
string collectionName = "hybrid-search";
|
||||
|
||||
if (await client.CollectionExistsAsync(collectionName))
|
||||
await client.DeleteCollectionAsync(collectionName);
|
||||
|
||||
await client.CreateCollectionAsync(
|
||||
collectionName: collectionName,
|
||||
vectorsConfig: new VectorParamsMap
|
||||
{
|
||||
Map =
|
||||
{
|
||||
["dense"] = new VectorParams
|
||||
{
|
||||
Size = 384,
|
||||
Distance = Distance.Cosine,
|
||||
},
|
||||
["multi"] = new VectorParams
|
||||
{
|
||||
Size = 96,
|
||||
Distance = Distance.Cosine,
|
||||
MultivectorConfig = new() { Comparator = MultiVectorComparator.MaxSim },
|
||||
HnswConfig = new HnswConfigDiff { M = 0 }, // Disable HNSW for reranking
|
||||
},
|
||||
}
|
||||
},
|
||||
sparseVectorsConfig: new SparseVectorConfig
|
||||
{
|
||||
Map =
|
||||
{
|
||||
["sparse"] = new SparseVectorParams { Modifier = Modifier.Idf }
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async IAsyncEnumerable<(string title, string author, string description)> ParseCsv(string url)
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
using var stream = await httpClient.GetStreamAsync(url);
|
||||
using var parser = new TextFieldParser(new StreamReader(stream));
|
||||
parser.TextFieldType = Microsoft.VisualBasic.FileIO.FieldType.Delimited;
|
||||
parser.SetDelimiters(",");
|
||||
string[]? headers = parser.ReadFields();
|
||||
int titleIdx = Array.IndexOf(headers!, "Title");
|
||||
int authorIdx = Array.IndexOf(headers!, "Author");
|
||||
int descriptionIdx = Array.IndexOf(headers!, "Description");
|
||||
while (!parser.EndOfData)
|
||||
{
|
||||
var fields = parser.ReadFields()!;
|
||||
yield return (fields[titleIdx], fields[authorIdx], fields[descriptionIdx]);
|
||||
}
|
||||
}
|
||||
|
||||
string csvUrl = "https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv";
|
||||
|
||||
int batchSize = 25;
|
||||
ulong idx = 0;
|
||||
var buffer = new List<PointStruct>();
|
||||
|
||||
await foreach (var (title, author, description) in ParseCsv(csvUrl))
|
||||
{
|
||||
buffer.Add(new PointStruct
|
||||
{
|
||||
Id = idx++,
|
||||
Vectors = new Dictionary<string, Vector>
|
||||
{
|
||||
["dense"] = new Document { Text = description, Model = denseEmbeddingModel },
|
||||
["sparse"] = new Document { Text = description, Model = sparseEmbeddingModel },
|
||||
["multi"] = new Document { Text = description, Model = lateInteractionEmbeddingModel },
|
||||
},
|
||||
Payload = { ["title"] = title, ["author"] = author, ["description"] = description }
|
||||
});
|
||||
|
||||
if (buffer.Count >= batchSize)
|
||||
{
|
||||
await client.UpsertAsync(collectionName: collectionName, points: buffer);
|
||||
buffer.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.Count > 0)
|
||||
await client.UpsertAsync(collectionName: collectionName, points: buffer);
|
||||
|
||||
string query = "time travel";
|
||||
|
||||
var results = await client.QueryAsync(
|
||||
collectionName: collectionName,
|
||||
query: new Document { Text = query, Model = denseEmbeddingModel },
|
||||
usingVector: "dense",
|
||||
limit: 10
|
||||
);
|
||||
|
||||
foreach (var result in results)
|
||||
Console.WriteLine(result);
|
||||
|
||||
results = await client.QueryAsync(
|
||||
collectionName: collectionName,
|
||||
query: new Document { Text = query, Model = sparseEmbeddingModel },
|
||||
usingVector: "sparse",
|
||||
limit: 10
|
||||
);
|
||||
|
||||
foreach (var result in results)
|
||||
Console.WriteLine(result);
|
||||
|
||||
results = await client.QueryAsync(
|
||||
collectionName: collectionName,
|
||||
prefetch: new List<PrefetchQuery>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Query = new Document { Text = query, Model = denseEmbeddingModel },
|
||||
Using = "dense",
|
||||
Limit = 20,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Query = new Document { Text = query, Model = sparseEmbeddingModel },
|
||||
Using = "sparse",
|
||||
Limit = 20,
|
||||
},
|
||||
},
|
||||
query: Fusion.Rrf,
|
||||
payloadSelector: true,
|
||||
limit: 10
|
||||
);
|
||||
|
||||
foreach (var result in results)
|
||||
Console.WriteLine(result);
|
||||
|
||||
results = await client.QueryAsync(
|
||||
collectionName: collectionName,
|
||||
prefetch: new List<PrefetchQuery>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Query = new Document { Text = query, Model = denseEmbeddingModel },
|
||||
Using = "dense",
|
||||
Limit = 20,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Query = new Document { Text = query, Model = sparseEmbeddingModel },
|
||||
Using = "sparse",
|
||||
Limit = 20,
|
||||
},
|
||||
},
|
||||
query: new Document { Text = query, Model = lateInteractionEmbeddingModel },
|
||||
usingVector: "multi",
|
||||
payloadSelector: true,
|
||||
limit: 10
|
||||
);
|
||||
|
||||
foreach (var result in results)
|
||||
Console.WriteLine(result);
|
||||
```
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
```csharp
|
||||
string denseEmbeddingModel = "sentence-transformers/all-MiniLM-L6-v2";
|
||||
string sparseEmbeddingModel = "qdrant/bm25";
|
||||
string lateInteractionEmbeddingModel = "answerdotai/answerai-colbert-small-v1";
|
||||
```
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
```go
|
||||
denseEmbeddingModel := "sentence-transformers/all-MiniLM-L6-v2"
|
||||
sparseEmbeddingModel := "qdrant/bm25"
|
||||
lateInteractionEmbeddingModel := "answerdotai/answerai-colbert-small-v1"
|
||||
```
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
```java
|
||||
String denseEmbeddingModel = "sentence-transformers/all-MiniLM-L6-v2";
|
||||
String sparseEmbeddingModel = "qdrant/bm25";
|
||||
String lateInteractionEmbeddingModel = "answerdotai/answerai-colbert-small-v1";
|
||||
```
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
```python
|
||||
dense_embedding_model = "sentence-transformers/all-MiniLM-L6-v2"
|
||||
sparse_embedding_model = "qdrant/bm25"
|
||||
late_interaction_embedding_model = "answerdotai/answerai-colbert-small-v1"
|
||||
```
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
```rust
|
||||
let dense_embedding_model = "sentence-transformers/all-MiniLM-L6-v2";
|
||||
let sparse_embedding_model = "qdrant/bm25";
|
||||
let late_interaction_embedding_model = "answerdotai/answerai-colbert-small-v1";
|
||||
```
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
```typescript
|
||||
const denseEmbeddingModel = "sentence-transformers/all-MiniLM-L6-v2";
|
||||
const sparseEmbeddingModel = "qdrant/bm25";
|
||||
const lateInteractionEmbeddingModel = "answerdotai/answerai-colbert-small-v1";
|
||||
```
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
```csharp
|
||||
string query = "time travel";
|
||||
|
||||
var results = await client.QueryAsync(
|
||||
collectionName: collectionName,
|
||||
query: new Document { Text = query, Model = denseEmbeddingModel },
|
||||
usingVector: "dense",
|
||||
limit: 10
|
||||
);
|
||||
|
||||
foreach (var result in results)
|
||||
Console.WriteLine(result);
|
||||
```
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
```go
|
||||
query := "time travel"
|
||||
|
||||
results, err := client.Query(context.Background(), &qdrant.QueryPoints{
|
||||
CollectionName: collectionName,
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: denseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("dense"),
|
||||
Limit: qdrant.PtrOf(uint64(10)),
|
||||
})
|
||||
|
||||
for _, result := range results {
|
||||
fmt.Println(result)
|
||||
}
|
||||
```
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
```java
|
||||
String query = "time travel";
|
||||
|
||||
var results = client.queryAsync(
|
||||
QueryPoints.newBuilder()
|
||||
.setCollectionName(collectionName)
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(denseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("dense")
|
||||
.setLimit(10)
|
||||
.build()
|
||||
).get();
|
||||
|
||||
for (var result : results) {
|
||||
System.out.println(result);
|
||||
}
|
||||
```
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
```python
|
||||
import pprint
|
||||
|
||||
query = "time travel"
|
||||
|
||||
results = client.query_points(
|
||||
collection_name,
|
||||
query=models.Document(text=query, model=dense_embedding_model),
|
||||
using="dense",
|
||||
limit=10,
|
||||
)
|
||||
|
||||
pprint.pp(results.points)
|
||||
```
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
```rust
|
||||
let query = "time travel";
|
||||
|
||||
let results = client
|
||||
.query(
|
||||
QueryPointsBuilder::new(collection_name)
|
||||
.query(Query::new_nearest(Document::new(query, dense_embedding_model)))
|
||||
.using("dense")
|
||||
.limit(10),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for result in results.result {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
```
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
```typescript
|
||||
const query = "time travel";
|
||||
|
||||
const denseResults = await client.query(collectionName, {
|
||||
query: { text: query, model: denseEmbeddingModel },
|
||||
using: "dense",
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
console.log(denseResults.points);
|
||||
```
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdrant/go-client/qdrant"
|
||||
)
|
||||
|
||||
type CSVRow struct {
|
||||
Title string
|
||||
Author string
|
||||
Description string
|
||||
}
|
||||
|
||||
func parseCSV(url string, fn func(CSVRow)) error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
csvReader := csv.NewReader(resp.Body)
|
||||
headers, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
titleIdx, authorIdx, descriptionIdx := -1, -1, -1
|
||||
for i, h := range headers {
|
||||
switch h {
|
||||
case "Title":
|
||||
titleIdx = i
|
||||
case "Author":
|
||||
authorIdx = i
|
||||
case "Description":
|
||||
descriptionIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
row, err := csvReader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fn(CSVRow{Title: row[titleIdx], Author: row[authorIdx], Description: row[descriptionIdx]})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
client, err := qdrant.NewClient(&qdrant.Config{
|
||||
Host: QDRANT_URL,
|
||||
APIKey: QDRANT_API_KEY,
|
||||
UseTLS: true,
|
||||
})
|
||||
|
||||
denseEmbeddingModel := "sentence-transformers/all-MiniLM-L6-v2"
|
||||
sparseEmbeddingModel := "qdrant/bm25"
|
||||
lateInteractionEmbeddingModel := "answerdotai/answerai-colbert-small-v1"
|
||||
|
||||
collectionName := "hybrid-search"
|
||||
|
||||
exists, err := client.CollectionExists(context.Background(), collectionName)
|
||||
if exists {
|
||||
client.DeleteCollection(context.Background(), collectionName)
|
||||
}
|
||||
|
||||
client.CreateCollection(context.Background(), &qdrant.CreateCollection{
|
||||
CollectionName: collectionName,
|
||||
VectorsConfig: qdrant.NewVectorsConfigMap(
|
||||
map[string]*qdrant.VectorParams{
|
||||
"dense": {
|
||||
Size: 384,
|
||||
Distance: qdrant.Distance_Cosine,
|
||||
},
|
||||
"multi": {
|
||||
Size: 96,
|
||||
Distance: qdrant.Distance_Cosine,
|
||||
MultivectorConfig: &qdrant.MultiVectorConfig{
|
||||
Comparator: qdrant.MultiVectorComparator_MaxSim,
|
||||
},
|
||||
HnswConfig: &qdrant.HnswConfigDiff{M: qdrant.PtrOf(uint64(0))}, // Disable HNSW for reranking
|
||||
},
|
||||
},
|
||||
),
|
||||
SparseVectorsConfig: qdrant.NewSparseVectorsConfig(
|
||||
map[string]*qdrant.SparseVectorParams{
|
||||
"sparse": {Modifier: qdrant.Modifier_Idf.Enum()},
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
csvUrl := "https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv"
|
||||
|
||||
batchSize := 25
|
||||
var idx uint64
|
||||
var buffer []*qdrant.PointStruct
|
||||
|
||||
err = parseCSV(csvUrl, func(row CSVRow) {
|
||||
title := row.Title
|
||||
author := row.Author
|
||||
description := row.Description
|
||||
|
||||
buffer = append(buffer, &qdrant.PointStruct{
|
||||
Id: qdrant.NewIDNum(idx),
|
||||
Vectors: qdrant.NewVectorsMap(map[string]*qdrant.Vector{
|
||||
"dense": qdrant.NewVectorDocument(&qdrant.Document{Text: description, Model: denseEmbeddingModel}),
|
||||
"sparse": qdrant.NewVectorDocument(&qdrant.Document{Text: description, Model: sparseEmbeddingModel}),
|
||||
"multi": qdrant.NewVectorDocument(&qdrant.Document{Text: description, Model: lateInteractionEmbeddingModel}),
|
||||
}),
|
||||
Payload: qdrant.NewValueMap(map[string]any{
|
||||
"title": title,
|
||||
"author": author,
|
||||
"description": description,
|
||||
}),
|
||||
})
|
||||
idx++
|
||||
|
||||
if len(buffer) >= batchSize {
|
||||
client.Upsert(context.Background(), &qdrant.UpsertPoints{
|
||||
CollectionName: collectionName,
|
||||
Points: buffer,
|
||||
})
|
||||
buffer = nil
|
||||
}
|
||||
})
|
||||
|
||||
if len(buffer) > 0 {
|
||||
client.Upsert(context.Background(), &qdrant.UpsertPoints{
|
||||
CollectionName: collectionName,
|
||||
Points: buffer,
|
||||
})
|
||||
}
|
||||
|
||||
query := "time travel"
|
||||
|
||||
results, err := client.Query(context.Background(), &qdrant.QueryPoints{
|
||||
CollectionName: collectionName,
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: denseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("dense"),
|
||||
Limit: qdrant.PtrOf(uint64(10)),
|
||||
})
|
||||
|
||||
for _, result := range results {
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
results, err = client.Query(context.Background(), &qdrant.QueryPoints{
|
||||
CollectionName: collectionName,
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: sparseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("sparse"),
|
||||
Limit: qdrant.PtrOf(uint64(10)),
|
||||
})
|
||||
|
||||
for _, result := range results {
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
results, err = client.Query(context.Background(), &qdrant.QueryPoints{
|
||||
CollectionName: collectionName,
|
||||
Prefetch: []*qdrant.PrefetchQuery{
|
||||
{
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: denseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("dense"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
{
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: sparseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("sparse"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
},
|
||||
Query: qdrant.NewQueryFusion(qdrant.Fusion_RRF),
|
||||
WithPayload: qdrant.NewWithPayload(true),
|
||||
Limit: qdrant.PtrOf(uint64(10)),
|
||||
})
|
||||
|
||||
for _, result := range results {
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
results, err = client.Query(context.Background(), &qdrant.QueryPoints{
|
||||
CollectionName: collectionName,
|
||||
Prefetch: []*qdrant.PrefetchQuery{
|
||||
{
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: denseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("dense"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
{
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: sparseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("sparse"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
},
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: lateInteractionEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("multi"),
|
||||
WithPayload: qdrant.NewWithPayload(true),
|
||||
Limit: qdrant.PtrOf(uint64(10)),
|
||||
})
|
||||
|
||||
for _, result := range results {
|
||||
fmt.Println(result)
|
||||
}
|
||||
```
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
```csharp
|
||||
results = await client.QueryAsync(
|
||||
collectionName: collectionName,
|
||||
prefetch: new List<PrefetchQuery>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Query = new Document { Text = query, Model = denseEmbeddingModel },
|
||||
Using = "dense",
|
||||
Limit = 20,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Query = new Document { Text = query, Model = sparseEmbeddingModel },
|
||||
Using = "sparse",
|
||||
Limit = 20,
|
||||
},
|
||||
},
|
||||
query: Fusion.Rrf,
|
||||
payloadSelector: true,
|
||||
limit: 10
|
||||
);
|
||||
|
||||
foreach (var result in results)
|
||||
Console.WriteLine(result);
|
||||
```
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
```go
|
||||
results, err = client.Query(context.Background(), &qdrant.QueryPoints{
|
||||
CollectionName: collectionName,
|
||||
Prefetch: []*qdrant.PrefetchQuery{
|
||||
{
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: denseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("dense"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
{
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: sparseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("sparse"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
},
|
||||
Query: qdrant.NewQueryFusion(qdrant.Fusion_RRF),
|
||||
WithPayload: qdrant.NewWithPayload(true),
|
||||
Limit: qdrant.PtrOf(uint64(10)),
|
||||
})
|
||||
|
||||
for _, result := range results {
|
||||
fmt.Println(result)
|
||||
}
|
||||
```
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
```java
|
||||
results = client.queryAsync(
|
||||
QueryPoints.newBuilder()
|
||||
.setCollectionName(collectionName)
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(denseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("dense")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(sparseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("sparse")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.setQuery(Query.newBuilder().setFusion(Fusion.RRF).build())
|
||||
.setWithPayload(enable(true))
|
||||
.setLimit(10)
|
||||
.build()
|
||||
).get();
|
||||
|
||||
for (var result : results) {
|
||||
System.out.println(result);
|
||||
}
|
||||
```
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
```python
|
||||
prefetch = [
|
||||
models.Prefetch(
|
||||
query=models.Document(text=query, model=dense_embedding_model),
|
||||
using="dense",
|
||||
limit=20,
|
||||
),
|
||||
models.Prefetch(
|
||||
query=models.Document(text=query, model=sparse_embedding_model),
|
||||
using="sparse",
|
||||
limit=20,
|
||||
),
|
||||
]
|
||||
|
||||
results = client.query_points(
|
||||
collection_name,
|
||||
prefetch=prefetch,
|
||||
query=models.FusionQuery(fusion=models.Fusion.RRF),
|
||||
with_payload=True,
|
||||
limit=10,
|
||||
)
|
||||
|
||||
pprint.pp(results.points)
|
||||
```
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
```rust
|
||||
let results = client
|
||||
.query(
|
||||
QueryPointsBuilder::new(collection_name)
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(Document::new(query, dense_embedding_model)))
|
||||
.using("dense")
|
||||
.limit(20u64),
|
||||
)
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(Document::new(query, sparse_embedding_model)))
|
||||
.using("sparse")
|
||||
.limit(20u64),
|
||||
)
|
||||
.query(Query::new_fusion(Fusion::Rrf))
|
||||
.with_payload(true)
|
||||
.limit(10),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for result in results.result {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
```
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
```typescript
|
||||
const hybridResults = await client.query(collectionName, {
|
||||
prefetch: [
|
||||
{
|
||||
query: { text: query, model: denseEmbeddingModel },
|
||||
using: "dense",
|
||||
limit: 20,
|
||||
},
|
||||
{
|
||||
query: { text: query, model: sparseEmbeddingModel },
|
||||
using: "sparse",
|
||||
limit: 20,
|
||||
},
|
||||
],
|
||||
query: { fusion: "rrf" },
|
||||
with_payload: true,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
console.log(hybridResults.points);
|
||||
```
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
```csharp
|
||||
string csvUrl = "https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv";
|
||||
|
||||
int batchSize = 25;
|
||||
ulong idx = 0;
|
||||
var buffer = new List<PointStruct>();
|
||||
|
||||
await foreach (var (title, author, description) in ParseCsv(csvUrl))
|
||||
{
|
||||
buffer.Add(new PointStruct
|
||||
{
|
||||
Id = idx++,
|
||||
Vectors = new Dictionary<string, Vector>
|
||||
{
|
||||
["dense"] = new Document { Text = description, Model = denseEmbeddingModel },
|
||||
["sparse"] = new Document { Text = description, Model = sparseEmbeddingModel },
|
||||
["multi"] = new Document { Text = description, Model = lateInteractionEmbeddingModel },
|
||||
},
|
||||
Payload = { ["title"] = title, ["author"] = author, ["description"] = description }
|
||||
});
|
||||
|
||||
if (buffer.Count >= batchSize)
|
||||
{
|
||||
await client.UpsertAsync(collectionName: collectionName, points: buffer);
|
||||
buffer.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.Count > 0)
|
||||
await client.UpsertAsync(collectionName: collectionName, points: buffer);
|
||||
```
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
```go
|
||||
csvUrl := "https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv"
|
||||
|
||||
batchSize := 25
|
||||
var idx uint64
|
||||
var buffer []*qdrant.PointStruct
|
||||
|
||||
err = parseCSV(csvUrl, func(row CSVRow) {
|
||||
title := row.Title
|
||||
author := row.Author
|
||||
description := row.Description
|
||||
|
||||
buffer = append(buffer, &qdrant.PointStruct{
|
||||
Id: qdrant.NewIDNum(idx),
|
||||
Vectors: qdrant.NewVectorsMap(map[string]*qdrant.Vector{
|
||||
"dense": qdrant.NewVectorDocument(&qdrant.Document{Text: description, Model: denseEmbeddingModel}),
|
||||
"sparse": qdrant.NewVectorDocument(&qdrant.Document{Text: description, Model: sparseEmbeddingModel}),
|
||||
"multi": qdrant.NewVectorDocument(&qdrant.Document{Text: description, Model: lateInteractionEmbeddingModel}),
|
||||
}),
|
||||
Payload: qdrant.NewValueMap(map[string]any{
|
||||
"title": title,
|
||||
"author": author,
|
||||
"description": description,
|
||||
}),
|
||||
})
|
||||
idx++
|
||||
|
||||
if len(buffer) >= batchSize {
|
||||
client.Upsert(context.Background(), &qdrant.UpsertPoints{
|
||||
CollectionName: collectionName,
|
||||
Points: buffer,
|
||||
})
|
||||
buffer = nil
|
||||
}
|
||||
})
|
||||
|
||||
if len(buffer) > 0 {
|
||||
client.Upsert(context.Background(), &qdrant.UpsertPoints{
|
||||
CollectionName: collectionName,
|
||||
Points: buffer,
|
||||
})
|
||||
}
|
||||
```
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
```java
|
||||
String csvUrl = "https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv";
|
||||
|
||||
int batchSize = 25;
|
||||
long idx = 0;
|
||||
List<PointStruct> buffer = new ArrayList<>();
|
||||
|
||||
try (var stream = parseCSV(csvUrl)) {
|
||||
for (var row : (Iterable<CsvRow>) stream::iterator) {
|
||||
String title = row.title;
|
||||
String author = row.author;
|
||||
String description = row.description;
|
||||
|
||||
buffer.add(
|
||||
PointStruct.newBuilder()
|
||||
.setId(io.qdrant.client.PointIdFactory.id(idx++))
|
||||
.setVectors(
|
||||
namedVectors(
|
||||
Map.of(
|
||||
"dense",
|
||||
vector(
|
||||
Document.newBuilder()
|
||||
.setText(description)
|
||||
.setModel(denseEmbeddingModel)
|
||||
.build()),
|
||||
"sparse",
|
||||
vector(
|
||||
Document.newBuilder()
|
||||
.setText(description)
|
||||
.setModel(sparseEmbeddingModel)
|
||||
.build()),
|
||||
"multi",
|
||||
vector(
|
||||
Document.newBuilder()
|
||||
.setText(description)
|
||||
.setModel(lateInteractionEmbeddingModel)
|
||||
.build()))))
|
||||
.putAllPayload(
|
||||
Map.of(
|
||||
"title", value(title),
|
||||
"author", value(author),
|
||||
"description", value(description)))
|
||||
.build());
|
||||
|
||||
if (buffer.size() >= batchSize) {
|
||||
client.upsertAsync(collectionName, buffer).get();
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!buffer.isEmpty()) {
|
||||
client.upsertAsync(collectionName, buffer).get();
|
||||
}
|
||||
```
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
```python
|
||||
from qdrant_client.models import Document, PointStruct
|
||||
|
||||
csv_url = 'https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv'
|
||||
|
||||
points = (
|
||||
PointStruct(
|
||||
id=idx,
|
||||
vector={
|
||||
"dense": Document(text=row['Description'], model=dense_embedding_model),
|
||||
"sparse": Document(text=row['Description'], model=sparse_embedding_model),
|
||||
"multi": Document(text=row['Description'], model=late_interaction_embedding_model),
|
||||
},
|
||||
payload={"title": row['Title'], "author": row['Author'], "description": row['Description']}
|
||||
)
|
||||
for idx, row in enumerate(parse_csv(csv_url))
|
||||
)
|
||||
client.upload_points(
|
||||
collection_name=collection_name,
|
||||
points=points,
|
||||
batch_size=25
|
||||
)
|
||||
```
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
```rust
|
||||
let csv_url = "https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv";
|
||||
|
||||
let batch_size = 25;
|
||||
let mut idx: u64 = 0;
|
||||
let mut buffer: Vec<PointStruct> = Vec::new();
|
||||
|
||||
for row in parse_csv(csv_url)? {
|
||||
let row = row?;
|
||||
let title = row.title;
|
||||
let author = row.author;
|
||||
let description = row.description;
|
||||
|
||||
let vectors = NamedVectors::default()
|
||||
.add_vector("dense", Document::new(&description, dense_embedding_model))
|
||||
.add_vector("sparse", Document::new(&description, sparse_embedding_model))
|
||||
.add_vector("multi", Document::new(&description, late_interaction_embedding_model));
|
||||
|
||||
buffer.push(PointStruct::new(
|
||||
idx,
|
||||
vectors,
|
||||
[
|
||||
("title", title.into()),
|
||||
("author", author.into()),
|
||||
("description", description.into()),
|
||||
],
|
||||
));
|
||||
idx += 1;
|
||||
|
||||
if buffer.len() >= batch_size {
|
||||
client
|
||||
.upsert_points(UpsertPointsBuilder::new(
|
||||
collection_name,
|
||||
std::mem::take(&mut buffer),
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if !buffer.is_empty() {
|
||||
client
|
||||
.upsert_points(UpsertPointsBuilder::new(collection_name, buffer))
|
||||
.await?;
|
||||
}
|
||||
```
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
```typescript
|
||||
const csvUrl = "https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv";
|
||||
|
||||
const batchSize = 25;
|
||||
let idx = 0;
|
||||
let buffer: Schemas["PointStruct"][] = [];
|
||||
|
||||
for await (const { title, author, description } of parseCSV(csvUrl)) {
|
||||
buffer.push({
|
||||
id: idx++,
|
||||
vector: {
|
||||
dense: { text: description, model: denseEmbeddingModel },
|
||||
sparse: { text: description, model: sparseEmbeddingModel },
|
||||
multi: { text: description, model: lateInteractionEmbeddingModel },
|
||||
},
|
||||
payload: { title, author, description },
|
||||
});
|
||||
|
||||
if (buffer.length >= batchSize) {
|
||||
await client.upsert(collectionName, { points: buffer });
|
||||
buffer = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.length > 0) {
|
||||
await client.upsert(collectionName, { points: buffer });
|
||||
}
|
||||
```
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
```java
|
||||
import static io.qdrant.client.QueryFactory.nearest;
|
||||
import static io.qdrant.client.ValueFactory.value;
|
||||
import static io.qdrant.client.VectorFactory.vector;
|
||||
import static io.qdrant.client.VectorsFactory.namedVectors;
|
||||
import static io.qdrant.client.WithPayloadSelectorFactory.enable;
|
||||
|
||||
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.HnswConfigDiff;
|
||||
import io.qdrant.client.grpc.Collections.Modifier;
|
||||
import io.qdrant.client.grpc.Collections.MultiVectorComparator;
|
||||
import io.qdrant.client.grpc.Collections.MultiVectorConfig;
|
||||
import io.qdrant.client.grpc.Collections.SparseVectorConfig;
|
||||
import io.qdrant.client.grpc.Collections.SparseVectorParams;
|
||||
import io.qdrant.client.grpc.Collections.VectorParams;
|
||||
import io.qdrant.client.grpc.Collections.VectorParamsMap;
|
||||
import io.qdrant.client.grpc.Collections.VectorsConfig;
|
||||
import io.qdrant.client.grpc.Points.Document;
|
||||
import io.qdrant.client.grpc.Points.Fusion;
|
||||
import io.qdrant.client.grpc.Points.PointStruct;
|
||||
import io.qdrant.client.grpc.Points.PrefetchQuery;
|
||||
import io.qdrant.client.grpc.Points.Query;
|
||||
import io.qdrant.client.grpc.Points.QueryPoints;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
static class CsvRow {
|
||||
final String title;
|
||||
final String author;
|
||||
final String description;
|
||||
CsvRow(String title, String author, String description) {
|
||||
this.title = title; this.author = author; this.description = description;
|
||||
}
|
||||
}
|
||||
|
||||
static Stream<CsvRow> parseCSV(String url) throws Exception {
|
||||
Function<String, List<String>> parseCsvLine = line -> {
|
||||
List<String> fields = new ArrayList<>();
|
||||
boolean inQuotes = false;
|
||||
var sb = new StringBuilder();
|
||||
for (char c : line.toCharArray()) {
|
||||
if (c == '"') {
|
||||
inQuotes = !inQuotes;
|
||||
} else if (c == ',' && !inQuotes) {
|
||||
fields.add(sb.toString());
|
||||
sb.setLength(0);
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
fields.add(sb.toString());
|
||||
return fields;
|
||||
};
|
||||
|
||||
var reader = new BufferedReader(new InputStreamReader(new URL(url).openStream()));
|
||||
String headerLine = reader.readLine();
|
||||
List<String> headers = parseCsvLine.apply(headerLine);
|
||||
int titleIdx = headers.indexOf("Title");
|
||||
int authorIdx = headers.indexOf("Author");
|
||||
int descriptionIdx = headers.indexOf("Description");
|
||||
|
||||
return reader.lines()
|
||||
.map(line -> {
|
||||
List<String> fields = parseCsvLine.apply(line);
|
||||
return new CsvRow(fields.get(titleIdx), fields.get(authorIdx), fields.get(descriptionIdx));
|
||||
})
|
||||
.onClose(() -> { try { reader.close(); } catch (Exception ignored) {} });
|
||||
}
|
||||
|
||||
QdrantClient client =
|
||||
new QdrantClient(
|
||||
QdrantGrpcClient.newBuilder(QDRANT_URL, 6334, true)
|
||||
.withApiKey(QDRANT_API_KEY)
|
||||
.build());
|
||||
|
||||
String denseEmbeddingModel = "sentence-transformers/all-MiniLM-L6-v2";
|
||||
String sparseEmbeddingModel = "qdrant/bm25";
|
||||
String lateInteractionEmbeddingModel = "answerdotai/answerai-colbert-small-v1";
|
||||
|
||||
String collectionName = "hybrid-search";
|
||||
|
||||
if (client.collectionExistsAsync(collectionName).get()) {
|
||||
client.deleteCollectionAsync(collectionName).get();
|
||||
}
|
||||
|
||||
client.createCollectionAsync(
|
||||
CreateCollection.newBuilder()
|
||||
.setCollectionName(collectionName)
|
||||
.setVectorsConfig(
|
||||
VectorsConfig.newBuilder()
|
||||
.setParamsMap(
|
||||
VectorParamsMap.newBuilder()
|
||||
.putMap(
|
||||
"dense",
|
||||
VectorParams.newBuilder()
|
||||
.setSize(384)
|
||||
.setDistance(Distance.Cosine)
|
||||
.build())
|
||||
.putMap(
|
||||
"multi",
|
||||
VectorParams.newBuilder()
|
||||
.setSize(96)
|
||||
.setDistance(Distance.Cosine)
|
||||
.setMultivectorConfig(
|
||||
MultiVectorConfig.newBuilder()
|
||||
.setComparator(MultiVectorComparator.MaxSim)
|
||||
.build())
|
||||
.setHnswConfig(
|
||||
HnswConfigDiff.newBuilder()
|
||||
.setM(0) // Disable HNSW for reranking
|
||||
.build())
|
||||
.build())
|
||||
.build()))
|
||||
.setSparseVectorsConfig(
|
||||
SparseVectorConfig.newBuilder()
|
||||
.putMap(
|
||||
"sparse",
|
||||
SparseVectorParams.newBuilder()
|
||||
.setModifier(Modifier.Idf)
|
||||
.build())
|
||||
.build())
|
||||
.build()
|
||||
).get();
|
||||
|
||||
String csvUrl = "https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv";
|
||||
|
||||
int batchSize = 25;
|
||||
long idx = 0;
|
||||
List<PointStruct> buffer = new ArrayList<>();
|
||||
|
||||
try (var stream = parseCSV(csvUrl)) {
|
||||
for (var row : (Iterable<CsvRow>) stream::iterator) {
|
||||
String title = row.title;
|
||||
String author = row.author;
|
||||
String description = row.description;
|
||||
|
||||
buffer.add(
|
||||
PointStruct.newBuilder()
|
||||
.setId(io.qdrant.client.PointIdFactory.id(idx++))
|
||||
.setVectors(
|
||||
namedVectors(
|
||||
Map.of(
|
||||
"dense",
|
||||
vector(
|
||||
Document.newBuilder()
|
||||
.setText(description)
|
||||
.setModel(denseEmbeddingModel)
|
||||
.build()),
|
||||
"sparse",
|
||||
vector(
|
||||
Document.newBuilder()
|
||||
.setText(description)
|
||||
.setModel(sparseEmbeddingModel)
|
||||
.build()),
|
||||
"multi",
|
||||
vector(
|
||||
Document.newBuilder()
|
||||
.setText(description)
|
||||
.setModel(lateInteractionEmbeddingModel)
|
||||
.build()))))
|
||||
.putAllPayload(
|
||||
Map.of(
|
||||
"title", value(title),
|
||||
"author", value(author),
|
||||
"description", value(description)))
|
||||
.build());
|
||||
|
||||
if (buffer.size() >= batchSize) {
|
||||
client.upsertAsync(collectionName, buffer).get();
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!buffer.isEmpty()) {
|
||||
client.upsertAsync(collectionName, buffer).get();
|
||||
}
|
||||
|
||||
String query = "time travel";
|
||||
|
||||
var results = client.queryAsync(
|
||||
QueryPoints.newBuilder()
|
||||
.setCollectionName(collectionName)
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(denseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("dense")
|
||||
.setLimit(10)
|
||||
.build()
|
||||
).get();
|
||||
|
||||
for (var result : results) {
|
||||
System.out.println(result);
|
||||
}
|
||||
|
||||
results = client.queryAsync(
|
||||
QueryPoints.newBuilder()
|
||||
.setCollectionName(collectionName)
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(sparseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("sparse")
|
||||
.setLimit(10)
|
||||
.build()
|
||||
).get();
|
||||
|
||||
for (var result : results) {
|
||||
System.out.println(result);
|
||||
}
|
||||
|
||||
results = client.queryAsync(
|
||||
QueryPoints.newBuilder()
|
||||
.setCollectionName(collectionName)
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(denseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("dense")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(sparseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("sparse")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.setQuery(Query.newBuilder().setFusion(Fusion.RRF).build())
|
||||
.setWithPayload(enable(true))
|
||||
.setLimit(10)
|
||||
.build()
|
||||
).get();
|
||||
|
||||
for (var result : results) {
|
||||
System.out.println(result);
|
||||
}
|
||||
|
||||
results = client.queryAsync(
|
||||
QueryPoints.newBuilder()
|
||||
.setCollectionName(collectionName)
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(denseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("dense")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(sparseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("sparse")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(lateInteractionEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("multi")
|
||||
.setWithPayload(enable(true))
|
||||
.setLimit(10)
|
||||
.build()
|
||||
).get();
|
||||
|
||||
for (var result : results) {
|
||||
System.out.println(result);
|
||||
}
|
||||
```
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
```csharp
|
||||
async IAsyncEnumerable<(string title, string author, string description)> ParseCsv(string url)
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
using var stream = await httpClient.GetStreamAsync(url);
|
||||
using var parser = new TextFieldParser(new StreamReader(stream));
|
||||
parser.TextFieldType = Microsoft.VisualBasic.FileIO.FieldType.Delimited;
|
||||
parser.SetDelimiters(",");
|
||||
string[]? headers = parser.ReadFields();
|
||||
int titleIdx = Array.IndexOf(headers!, "Title");
|
||||
int authorIdx = Array.IndexOf(headers!, "Author");
|
||||
int descriptionIdx = Array.IndexOf(headers!, "Description");
|
||||
while (!parser.EndOfData)
|
||||
{
|
||||
var fields = parser.ReadFields()!;
|
||||
yield return (fields[titleIdx], fields[authorIdx], fields[descriptionIdx]);
|
||||
}
|
||||
}
|
||||
```
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
```go
|
||||
type CSVRow struct {
|
||||
Title string
|
||||
Author string
|
||||
Description string
|
||||
}
|
||||
|
||||
func parseCSV(url string, fn func(CSVRow)) error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
csvReader := csv.NewReader(resp.Body)
|
||||
headers, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
titleIdx, authorIdx, descriptionIdx := -1, -1, -1
|
||||
for i, h := range headers {
|
||||
switch h {
|
||||
case "Title":
|
||||
titleIdx = i
|
||||
case "Author":
|
||||
authorIdx = i
|
||||
case "Description":
|
||||
descriptionIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
row, err := csvReader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fn(CSVRow{Title: row[titleIdx], Author: row[authorIdx], Description: row[descriptionIdx]})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
```java
|
||||
static class CsvRow {
|
||||
final String title;
|
||||
final String author;
|
||||
final String description;
|
||||
CsvRow(String title, String author, String description) {
|
||||
this.title = title; this.author = author; this.description = description;
|
||||
}
|
||||
}
|
||||
|
||||
static Stream<CsvRow> parseCSV(String url) throws Exception {
|
||||
Function<String, List<String>> parseCsvLine = line -> {
|
||||
List<String> fields = new ArrayList<>();
|
||||
boolean inQuotes = false;
|
||||
var sb = new StringBuilder();
|
||||
for (char c : line.toCharArray()) {
|
||||
if (c == '"') {
|
||||
inQuotes = !inQuotes;
|
||||
} else if (c == ',' && !inQuotes) {
|
||||
fields.add(sb.toString());
|
||||
sb.setLength(0);
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
fields.add(sb.toString());
|
||||
return fields;
|
||||
};
|
||||
|
||||
var reader = new BufferedReader(new InputStreamReader(new URL(url).openStream()));
|
||||
String headerLine = reader.readLine();
|
||||
List<String> headers = parseCsvLine.apply(headerLine);
|
||||
int titleIdx = headers.indexOf("Title");
|
||||
int authorIdx = headers.indexOf("Author");
|
||||
int descriptionIdx = headers.indexOf("Description");
|
||||
|
||||
return reader.lines()
|
||||
.map(line -> {
|
||||
List<String> fields = parseCsvLine.apply(line);
|
||||
return new CsvRow(fields.get(titleIdx), fields.get(authorIdx), fields.get(descriptionIdx));
|
||||
})
|
||||
.onClose(() -> { try { reader.close(); } catch (Exception ignored) {} });
|
||||
}
|
||||
```
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
```python
|
||||
import csv
|
||||
import urllib.request
|
||||
|
||||
def parse_csv(url):
|
||||
with urllib.request.urlopen(url) as response:
|
||||
reader = csv.DictReader(line.decode('utf-8') for line in response)
|
||||
yield from reader
|
||||
```
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
```rust
|
||||
struct CsvRow {
|
||||
title: String,
|
||||
author: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
fn parse_csv(url: &str) -> anyhow::Result<impl Iterator<Item = anyhow::Result<CsvRow>>> {
|
||||
let reader = ureq::get(url).call()?.into_body().into_reader();
|
||||
let mut rdr = csv::Reader::from_reader(reader);
|
||||
let headers = rdr.headers()?.clone();
|
||||
let title_idx = headers.iter().position(|h| h == "Title").unwrap();
|
||||
let author_idx = headers.iter().position(|h| h == "Author").unwrap();
|
||||
let description_idx = headers.iter().position(|h| h == "Description").unwrap();
|
||||
let iter = rdr.into_records().map(move |result| {
|
||||
let record = result?;
|
||||
Ok(CsvRow {
|
||||
title: record[title_idx].to_string(),
|
||||
author: record[author_idx].to_string(),
|
||||
description: record[description_idx].to_string(),
|
||||
})
|
||||
});
|
||||
Ok(iter)
|
||||
}
|
||||
```
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
```typescript
|
||||
function parseCsvLine(line: string): string[] {
|
||||
const fields: string[] = [];
|
||||
let i = 0;
|
||||
while (i < line.length) {
|
||||
if (line[i] === '"') {
|
||||
i++;
|
||||
let field = "";
|
||||
while (i < line.length) {
|
||||
if (line[i] === '"' && line[i + 1] === '"') { field += '"'; i += 2; }
|
||||
else if (line[i] === '"') { i++; break; }
|
||||
else { field += line[i++]; }
|
||||
}
|
||||
fields.push(field);
|
||||
if (line[i] === ",") i++;
|
||||
} else {
|
||||
const start = i;
|
||||
while (i < line.length && line[i] !== ",") i++;
|
||||
fields.push(line.slice(start, i));
|
||||
if (i < line.length) i++;
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
async function* parseCSV(url: string): AsyncGenerator<{ title: string; author: string; description: string }> {
|
||||
const response = await fetch(url);
|
||||
const reader = response.body!.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let remainder = "";
|
||||
let headers: string[] | null = null;
|
||||
let titleIdx = -1;
|
||||
let authorIdx = -1;
|
||||
let descriptionIdx = -1;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
const chunk = done ? "" : decoder.decode(value, { stream: true });
|
||||
const lines = (remainder + chunk).split("\n");
|
||||
remainder = done ? "" : lines.pop()!;
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
if (headers === null) {
|
||||
headers = parseCsvLine(line);
|
||||
titleIdx = headers.indexOf("Title");
|
||||
authorIdx = headers.indexOf("Author");
|
||||
descriptionIdx = headers.indexOf("Description");
|
||||
continue;
|
||||
}
|
||||
const fields = parseCsvLine(line);
|
||||
yield { title: fields[titleIdx], author: fields[authorIdx], description: fields[descriptionIdx] };
|
||||
}
|
||||
|
||||
if (done) break;
|
||||
}
|
||||
}
|
||||
```
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
```python
|
||||
from qdrant_client import QdrantClient
|
||||
|
||||
client = QdrantClient(
|
||||
url="https://xyz-example.eu-central.aws.cloud.qdrant.io:6333",
|
||||
api_key="<your-api-key>",
|
||||
cloud_inference=True,
|
||||
)
|
||||
|
||||
dense_embedding_model = "sentence-transformers/all-MiniLM-L6-v2"
|
||||
sparse_embedding_model = "qdrant/bm25"
|
||||
late_interaction_embedding_model = "answerdotai/answerai-colbert-small-v1"
|
||||
|
||||
from qdrant_client.models import Distance, VectorParams, models
|
||||
|
||||
collection_name = "hybrid-search"
|
||||
|
||||
if client.collection_exists(collection_name=collection_name):
|
||||
client.delete_collection(collection_name=collection_name)
|
||||
|
||||
client.create_collection(
|
||||
collection_name,
|
||||
vectors_config={
|
||||
"dense": models.VectorParams(
|
||||
size=384,
|
||||
distance=models.Distance.COSINE,
|
||||
),
|
||||
"multi": models.VectorParams(
|
||||
size=96,
|
||||
distance=models.Distance.COSINE,
|
||||
multivector_config=models.MultiVectorConfig(
|
||||
comparator=models.MultiVectorComparator.MAX_SIM,
|
||||
),
|
||||
hnsw_config=models.HnswConfigDiff(m=0) # Disable HNSW for reranking
|
||||
),
|
||||
},
|
||||
sparse_vectors_config={
|
||||
"sparse": models.SparseVectorParams(modifier=models.Modifier.IDF)
|
||||
}
|
||||
)
|
||||
|
||||
import csv
|
||||
import urllib.request
|
||||
|
||||
def parse_csv(url):
|
||||
with urllib.request.urlopen(url) as response:
|
||||
reader = csv.DictReader(line.decode('utf-8') for line in response)
|
||||
yield from reader
|
||||
|
||||
from qdrant_client.models import Document, PointStruct
|
||||
|
||||
csv_url = 'https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv'
|
||||
|
||||
points = (
|
||||
PointStruct(
|
||||
id=idx,
|
||||
vector={
|
||||
"dense": Document(text=row['Description'], model=dense_embedding_model),
|
||||
"sparse": Document(text=row['Description'], model=sparse_embedding_model),
|
||||
"multi": Document(text=row['Description'], model=late_interaction_embedding_model),
|
||||
},
|
||||
payload={"title": row['Title'], "author": row['Author'], "description": row['Description']}
|
||||
)
|
||||
for idx, row in enumerate(parse_csv(csv_url))
|
||||
)
|
||||
client.upload_points(
|
||||
collection_name=collection_name,
|
||||
points=points,
|
||||
batch_size=25
|
||||
)
|
||||
|
||||
import pprint
|
||||
|
||||
query = "time travel"
|
||||
|
||||
results = client.query_points(
|
||||
collection_name,
|
||||
query=models.Document(text=query, model=dense_embedding_model),
|
||||
using="dense",
|
||||
limit=10,
|
||||
)
|
||||
|
||||
pprint.pp(results.points)
|
||||
|
||||
results = client.query_points(
|
||||
collection_name,
|
||||
query=models.Document(text=query, model=sparse_embedding_model),
|
||||
using="sparse",
|
||||
limit=10,
|
||||
)
|
||||
|
||||
pprint.pp(results.points)
|
||||
|
||||
prefetch = [
|
||||
models.Prefetch(
|
||||
query=models.Document(text=query, model=dense_embedding_model),
|
||||
using="dense",
|
||||
limit=20,
|
||||
),
|
||||
models.Prefetch(
|
||||
query=models.Document(text=query, model=sparse_embedding_model),
|
||||
using="sparse",
|
||||
limit=20,
|
||||
),
|
||||
]
|
||||
|
||||
results = client.query_points(
|
||||
collection_name,
|
||||
prefetch=prefetch,
|
||||
query=models.FusionQuery(fusion=models.Fusion.RRF),
|
||||
with_payload=True,
|
||||
limit=10,
|
||||
)
|
||||
|
||||
pprint.pp(results.points)
|
||||
|
||||
prefetch = [
|
||||
models.Prefetch(
|
||||
query=models.Document(text=query, model=dense_embedding_model),
|
||||
using="dense",
|
||||
limit=20,
|
||||
),
|
||||
models.Prefetch(
|
||||
query=models.Document(text=query, model=sparse_embedding_model),
|
||||
using="sparse",
|
||||
limit=20,
|
||||
),
|
||||
]
|
||||
|
||||
results = client.query_points(
|
||||
collection_name,
|
||||
prefetch=prefetch,
|
||||
query=models.Document(text=query, model=late_interaction_embedding_model),
|
||||
using="multi",
|
||||
with_payload=True,
|
||||
limit=10,
|
||||
)
|
||||
|
||||
pprint.pp(results.points)
|
||||
```
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
```csharp
|
||||
results = await client.QueryAsync(
|
||||
collectionName: collectionName,
|
||||
prefetch: new List<PrefetchQuery>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Query = new Document { Text = query, Model = denseEmbeddingModel },
|
||||
Using = "dense",
|
||||
Limit = 20,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Query = new Document { Text = query, Model = sparseEmbeddingModel },
|
||||
Using = "sparse",
|
||||
Limit = 20,
|
||||
},
|
||||
},
|
||||
query: new Document { Text = query, Model = lateInteractionEmbeddingModel },
|
||||
usingVector: "multi",
|
||||
payloadSelector: true,
|
||||
limit: 10
|
||||
);
|
||||
|
||||
foreach (var result in results)
|
||||
Console.WriteLine(result);
|
||||
```
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
```go
|
||||
results, err = client.Query(context.Background(), &qdrant.QueryPoints{
|
||||
CollectionName: collectionName,
|
||||
Prefetch: []*qdrant.PrefetchQuery{
|
||||
{
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: denseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("dense"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
{
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: sparseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("sparse"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
},
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: lateInteractionEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("multi"),
|
||||
WithPayload: qdrant.NewWithPayload(true),
|
||||
Limit: qdrant.PtrOf(uint64(10)),
|
||||
})
|
||||
|
||||
for _, result := range results {
|
||||
fmt.Println(result)
|
||||
}
|
||||
```
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
```java
|
||||
results = client.queryAsync(
|
||||
QueryPoints.newBuilder()
|
||||
.setCollectionName(collectionName)
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(denseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("dense")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(sparseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("sparse")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(lateInteractionEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("multi")
|
||||
.setWithPayload(enable(true))
|
||||
.setLimit(10)
|
||||
.build()
|
||||
).get();
|
||||
|
||||
for (var result : results) {
|
||||
System.out.println(result);
|
||||
}
|
||||
```
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
```python
|
||||
prefetch = [
|
||||
models.Prefetch(
|
||||
query=models.Document(text=query, model=dense_embedding_model),
|
||||
using="dense",
|
||||
limit=20,
|
||||
),
|
||||
models.Prefetch(
|
||||
query=models.Document(text=query, model=sparse_embedding_model),
|
||||
using="sparse",
|
||||
limit=20,
|
||||
),
|
||||
]
|
||||
|
||||
results = client.query_points(
|
||||
collection_name,
|
||||
prefetch=prefetch,
|
||||
query=models.Document(text=query, model=late_interaction_embedding_model),
|
||||
using="multi",
|
||||
with_payload=True,
|
||||
limit=10,
|
||||
)
|
||||
|
||||
pprint.pp(results.points)
|
||||
```
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
```rust
|
||||
let results = client
|
||||
.query(
|
||||
QueryPointsBuilder::new(collection_name)
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(Document::new(query, dense_embedding_model)))
|
||||
.using("dense")
|
||||
.limit(20u64),
|
||||
)
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(Document::new(query, sparse_embedding_model)))
|
||||
.using("sparse")
|
||||
.limit(20u64),
|
||||
)
|
||||
.query(Query::new_nearest(Document::new(query, late_interaction_embedding_model)))
|
||||
.using("multi")
|
||||
.with_payload(true)
|
||||
.limit(10),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for result in results.result {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
```
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
```typescript
|
||||
const rerankedResults = await client.query(collectionName, {
|
||||
prefetch: [
|
||||
{
|
||||
query: { text: query, model: denseEmbeddingModel },
|
||||
using: "dense",
|
||||
limit: 20,
|
||||
},
|
||||
{
|
||||
query: { text: query, model: sparseEmbeddingModel },
|
||||
using: "sparse",
|
||||
limit: 20,
|
||||
},
|
||||
],
|
||||
query: { text: query, model: lateInteractionEmbeddingModel },
|
||||
using: "multi",
|
||||
with_payload: true,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
console.log(rerankedResults.points);
|
||||
```
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
```rust
|
||||
use qdrant_client::Qdrant;
|
||||
use qdrant_client::qdrant::{
|
||||
CreateCollectionBuilder, Distance, Document, Fusion, HnswConfigDiffBuilder,
|
||||
Modifier, MultiVectorComparator, MultiVectorConfigBuilder, NamedVectors, PointStruct,
|
||||
PrefetchQueryBuilder, Query, QueryPointsBuilder, SparseVectorParamsBuilder,
|
||||
SparseVectorsConfigBuilder, UpsertPointsBuilder, VectorParamsBuilder, VectorsConfigBuilder,
|
||||
};
|
||||
|
||||
let client = Qdrant::from_url(qdrant_url)
|
||||
.api_key(qdrant_api_key)
|
||||
.build()?;
|
||||
|
||||
let dense_embedding_model = "sentence-transformers/all-MiniLM-L6-v2";
|
||||
let sparse_embedding_model = "qdrant/bm25";
|
||||
let late_interaction_embedding_model = "answerdotai/answerai-colbert-small-v1";
|
||||
|
||||
let collection_name = "hybrid-search";
|
||||
|
||||
if client.collection_exists(collection_name).await? {
|
||||
client.delete_collection(collection_name).await?;
|
||||
}
|
||||
|
||||
let mut vectors = VectorsConfigBuilder::default();
|
||||
vectors.add_named_vector_params(
|
||||
"dense",
|
||||
VectorParamsBuilder::new(384, Distance::Cosine),
|
||||
);
|
||||
vectors.add_named_vector_params(
|
||||
"multi",
|
||||
VectorParamsBuilder::new(96, Distance::Cosine)
|
||||
.multivector_config(MultiVectorConfigBuilder::new(MultiVectorComparator::MaxSim))
|
||||
.hnsw_config(HnswConfigDiffBuilder::default().m(0)), // Disable HNSW for reranking
|
||||
);
|
||||
|
||||
let mut sparse = SparseVectorsConfigBuilder::default();
|
||||
sparse.add_named_vector_params(
|
||||
"sparse",
|
||||
SparseVectorParamsBuilder::default().modifier(Modifier::Idf),
|
||||
);
|
||||
|
||||
client
|
||||
.create_collection(
|
||||
CreateCollectionBuilder::new(collection_name)
|
||||
.vectors_config(vectors)
|
||||
.sparse_vectors_config(sparse),
|
||||
)
|
||||
.await?;
|
||||
|
||||
struct CsvRow {
|
||||
title: String,
|
||||
author: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
fn parse_csv(url: &str) -> anyhow::Result<impl Iterator<Item = anyhow::Result<CsvRow>>> {
|
||||
let reader = ureq::get(url).call()?.into_body().into_reader();
|
||||
let mut rdr = csv::Reader::from_reader(reader);
|
||||
let headers = rdr.headers()?.clone();
|
||||
let title_idx = headers.iter().position(|h| h == "Title").unwrap();
|
||||
let author_idx = headers.iter().position(|h| h == "Author").unwrap();
|
||||
let description_idx = headers.iter().position(|h| h == "Description").unwrap();
|
||||
let iter = rdr.into_records().map(move |result| {
|
||||
let record = result?;
|
||||
Ok(CsvRow {
|
||||
title: record[title_idx].to_string(),
|
||||
author: record[author_idx].to_string(),
|
||||
description: record[description_idx].to_string(),
|
||||
})
|
||||
});
|
||||
Ok(iter)
|
||||
}
|
||||
|
||||
let csv_url = "https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv";
|
||||
|
||||
let batch_size = 25;
|
||||
let mut idx: u64 = 0;
|
||||
let mut buffer: Vec<PointStruct> = Vec::new();
|
||||
|
||||
for row in parse_csv(csv_url)? {
|
||||
let row = row?;
|
||||
let title = row.title;
|
||||
let author = row.author;
|
||||
let description = row.description;
|
||||
|
||||
let vectors = NamedVectors::default()
|
||||
.add_vector("dense", Document::new(&description, dense_embedding_model))
|
||||
.add_vector("sparse", Document::new(&description, sparse_embedding_model))
|
||||
.add_vector("multi", Document::new(&description, late_interaction_embedding_model));
|
||||
|
||||
buffer.push(PointStruct::new(
|
||||
idx,
|
||||
vectors,
|
||||
[
|
||||
("title", title.into()),
|
||||
("author", author.into()),
|
||||
("description", description.into()),
|
||||
],
|
||||
));
|
||||
idx += 1;
|
||||
|
||||
if buffer.len() >= batch_size {
|
||||
client
|
||||
.upsert_points(UpsertPointsBuilder::new(
|
||||
collection_name,
|
||||
std::mem::take(&mut buffer),
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if !buffer.is_empty() {
|
||||
client
|
||||
.upsert_points(UpsertPointsBuilder::new(collection_name, buffer))
|
||||
.await?;
|
||||
}
|
||||
|
||||
let query = "time travel";
|
||||
|
||||
let results = client
|
||||
.query(
|
||||
QueryPointsBuilder::new(collection_name)
|
||||
.query(Query::new_nearest(Document::new(query, dense_embedding_model)))
|
||||
.using("dense")
|
||||
.limit(10),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for result in results.result {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
|
||||
let results = client
|
||||
.query(
|
||||
QueryPointsBuilder::new(collection_name)
|
||||
.query(Query::new_nearest(Document::new(query, sparse_embedding_model)))
|
||||
.using("sparse")
|
||||
.limit(10),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for result in results.result {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
|
||||
let results = client
|
||||
.query(
|
||||
QueryPointsBuilder::new(collection_name)
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(Document::new(query, dense_embedding_model)))
|
||||
.using("dense")
|
||||
.limit(20u64),
|
||||
)
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(Document::new(query, sparse_embedding_model)))
|
||||
.using("sparse")
|
||||
.limit(20u64),
|
||||
)
|
||||
.query(Query::new_fusion(Fusion::Rrf))
|
||||
.with_payload(true)
|
||||
.limit(10),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for result in results.result {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
|
||||
let results = client
|
||||
.query(
|
||||
QueryPointsBuilder::new(collection_name)
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(Document::new(query, dense_embedding_model)))
|
||||
.using("dense")
|
||||
.limit(20u64),
|
||||
)
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(Document::new(query, sparse_embedding_model)))
|
||||
.using("sparse")
|
||||
.limit(20u64),
|
||||
)
|
||||
.query(Query::new_nearest(Document::new(query, late_interaction_embedding_model)))
|
||||
.using("multi")
|
||||
.with_payload(true)
|
||||
.limit(10),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for result in results.result {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
```
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
```csharp
|
||||
results = await client.QueryAsync(
|
||||
collectionName: collectionName,
|
||||
query: new Document { Text = query, Model = sparseEmbeddingModel },
|
||||
usingVector: "sparse",
|
||||
limit: 10
|
||||
);
|
||||
|
||||
foreach (var result in results)
|
||||
Console.WriteLine(result);
|
||||
```
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
```go
|
||||
results, err = client.Query(context.Background(), &qdrant.QueryPoints{
|
||||
CollectionName: collectionName,
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: sparseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("sparse"),
|
||||
Limit: qdrant.PtrOf(uint64(10)),
|
||||
})
|
||||
|
||||
for _, result := range results {
|
||||
fmt.Println(result)
|
||||
}
|
||||
```
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
```java
|
||||
results = client.queryAsync(
|
||||
QueryPoints.newBuilder()
|
||||
.setCollectionName(collectionName)
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(sparseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("sparse")
|
||||
.setLimit(10)
|
||||
.build()
|
||||
).get();
|
||||
|
||||
for (var result : results) {
|
||||
System.out.println(result);
|
||||
}
|
||||
```
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
```python
|
||||
results = client.query_points(
|
||||
collection_name,
|
||||
query=models.Document(text=query, model=sparse_embedding_model),
|
||||
using="sparse",
|
||||
limit=10,
|
||||
)
|
||||
|
||||
pprint.pp(results.points)
|
||||
```
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
```rust
|
||||
let results = client
|
||||
.query(
|
||||
QueryPointsBuilder::new(collection_name)
|
||||
.query(Query::new_nearest(Document::new(query, sparse_embedding_model)))
|
||||
.using("sparse")
|
||||
.limit(10),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for result in results.result {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
```
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
```typescript
|
||||
const sparseResults = await client.query(collectionName, {
|
||||
query: { text: query, model: sparseEmbeddingModel },
|
||||
using: "sparse",
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
console.log(sparseResults.points);
|
||||
```
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
```typescript
|
||||
import { QdrantClient, Schemas } from "@qdrant/js-client-rest";
|
||||
|
||||
const client = new QdrantClient({
|
||||
url: QDRANT_URL,
|
||||
apiKey: QDRANT_API_KEY,
|
||||
});
|
||||
|
||||
const denseEmbeddingModel = "sentence-transformers/all-MiniLM-L6-v2";
|
||||
const sparseEmbeddingModel = "qdrant/bm25";
|
||||
const lateInteractionEmbeddingModel = "answerdotai/answerai-colbert-small-v1";
|
||||
|
||||
const collectionName = "hybrid-search";
|
||||
|
||||
if (await client.collectionExists(collectionName)) {
|
||||
await client.deleteCollection(collectionName);
|
||||
}
|
||||
|
||||
await client.createCollection(collectionName, {
|
||||
vectors: {
|
||||
dense: {
|
||||
size: 384,
|
||||
distance: "Cosine",
|
||||
},
|
||||
multi: {
|
||||
size: 96,
|
||||
distance: "Cosine",
|
||||
multivector_config: { comparator: "max_sim" },
|
||||
hnsw_config: { m: 0 }, // Disable HNSW for reranking
|
||||
},
|
||||
},
|
||||
sparse_vectors: {
|
||||
sparse: { modifier: "idf" },
|
||||
},
|
||||
});
|
||||
|
||||
function parseCsvLine(line: string): string[] {
|
||||
const fields: string[] = [];
|
||||
let i = 0;
|
||||
while (i < line.length) {
|
||||
if (line[i] === '"') {
|
||||
i++;
|
||||
let field = "";
|
||||
while (i < line.length) {
|
||||
if (line[i] === '"' && line[i + 1] === '"') { field += '"'; i += 2; }
|
||||
else if (line[i] === '"') { i++; break; }
|
||||
else { field += line[i++]; }
|
||||
}
|
||||
fields.push(field);
|
||||
if (line[i] === ",") i++;
|
||||
} else {
|
||||
const start = i;
|
||||
while (i < line.length && line[i] !== ",") i++;
|
||||
fields.push(line.slice(start, i));
|
||||
if (i < line.length) i++;
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
async function* parseCSV(url: string): AsyncGenerator<{ title: string; author: string; description: string }> {
|
||||
const response = await fetch(url);
|
||||
const reader = response.body!.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let remainder = "";
|
||||
let headers: string[] | null = null;
|
||||
let titleIdx = -1;
|
||||
let authorIdx = -1;
|
||||
let descriptionIdx = -1;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
const chunk = done ? "" : decoder.decode(value, { stream: true });
|
||||
const lines = (remainder + chunk).split("\n");
|
||||
remainder = done ? "" : lines.pop()!;
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
if (headers === null) {
|
||||
headers = parseCsvLine(line);
|
||||
titleIdx = headers.indexOf("Title");
|
||||
authorIdx = headers.indexOf("Author");
|
||||
descriptionIdx = headers.indexOf("Description");
|
||||
continue;
|
||||
}
|
||||
const fields = parseCsvLine(line);
|
||||
yield { title: fields[titleIdx], author: fields[authorIdx], description: fields[descriptionIdx] };
|
||||
}
|
||||
|
||||
if (done) break;
|
||||
}
|
||||
}
|
||||
|
||||
const csvUrl = "https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv";
|
||||
|
||||
const batchSize = 25;
|
||||
let idx = 0;
|
||||
let buffer: Schemas["PointStruct"][] = [];
|
||||
|
||||
for await (const { title, author, description } of parseCSV(csvUrl)) {
|
||||
buffer.push({
|
||||
id: idx++,
|
||||
vector: {
|
||||
dense: { text: description, model: denseEmbeddingModel },
|
||||
sparse: { text: description, model: sparseEmbeddingModel },
|
||||
multi: { text: description, model: lateInteractionEmbeddingModel },
|
||||
},
|
||||
payload: { title, author, description },
|
||||
});
|
||||
|
||||
if (buffer.length >= batchSize) {
|
||||
await client.upsert(collectionName, { points: buffer });
|
||||
buffer = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.length > 0) {
|
||||
await client.upsert(collectionName, { points: buffer });
|
||||
}
|
||||
|
||||
const query = "time travel";
|
||||
|
||||
const denseResults = await client.query(collectionName, {
|
||||
query: { text: query, model: denseEmbeddingModel },
|
||||
using: "dense",
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
console.log(denseResults.points);
|
||||
|
||||
const sparseResults = await client.query(collectionName, {
|
||||
query: { text: query, model: sparseEmbeddingModel },
|
||||
using: "sparse",
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
console.log(sparseResults.points);
|
||||
|
||||
const hybridResults = await client.query(collectionName, {
|
||||
prefetch: [
|
||||
{
|
||||
query: { text: query, model: denseEmbeddingModel },
|
||||
using: "dense",
|
||||
limit: 20,
|
||||
},
|
||||
{
|
||||
query: { text: query, model: sparseEmbeddingModel },
|
||||
using: "sparse",
|
||||
limit: 20,
|
||||
},
|
||||
],
|
||||
query: { fusion: "rrf" },
|
||||
with_payload: true,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
console.log(hybridResults.points);
|
||||
|
||||
const rerankedResults = await client.query(collectionName, {
|
||||
prefetch: [
|
||||
{
|
||||
query: { text: query, model: denseEmbeddingModel },
|
||||
using: "dense",
|
||||
limit: 20,
|
||||
},
|
||||
{
|
||||
query: { text: query, model: sparseEmbeddingModel },
|
||||
using: "sparse",
|
||||
limit: 20,
|
||||
},
|
||||
],
|
||||
query: { text: query, model: lateInteractionEmbeddingModel },
|
||||
using: "multi",
|
||||
with_payload: true,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
console.log(rerankedResults.points);
|
||||
```
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
package snippet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdrant/go-client/qdrant"
|
||||
)
|
||||
|
||||
// @block-start parse-csv
|
||||
type CSVRow struct {
|
||||
Title string
|
||||
Author string
|
||||
Description string
|
||||
}
|
||||
|
||||
func parseCSV(url string, fn func(CSVRow)) error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
csvReader := csv.NewReader(resp.Body)
|
||||
headers, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
titleIdx, authorIdx, descriptionIdx := -1, -1, -1
|
||||
for i, h := range headers {
|
||||
switch h {
|
||||
case "Title":
|
||||
titleIdx = i
|
||||
case "Author":
|
||||
authorIdx = i
|
||||
case "Description":
|
||||
descriptionIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
row, err := csvReader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fn(CSVRow{Title: row[titleIdx], Author: row[authorIdx], Description: row[descriptionIdx]})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// @block-end parse-csv
|
||||
|
||||
func Main() {
|
||||
// @hide-start
|
||||
QDRANT_URL := "xyz-example.eu-central.aws.cloud.qdrant.io"
|
||||
QDRANT_API_KEY := "<your-api-key>"
|
||||
// @hide-end
|
||||
// @block-start client-connection
|
||||
client, err := qdrant.NewClient(&qdrant.Config{
|
||||
Host: QDRANT_URL,
|
||||
APIKey: QDRANT_API_KEY,
|
||||
UseTLS: true,
|
||||
})
|
||||
// @block-end client-connection
|
||||
|
||||
// @hide-start
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// @hide-end
|
||||
|
||||
// @block-start define-models
|
||||
denseEmbeddingModel := "sentence-transformers/all-MiniLM-L6-v2"
|
||||
sparseEmbeddingModel := "qdrant/bm25"
|
||||
lateInteractionEmbeddingModel := "answerdotai/answerai-colbert-small-v1"
|
||||
// @block-end define-models
|
||||
|
||||
// @block-start create-collection
|
||||
collectionName := "hybrid-search"
|
||||
|
||||
exists, err := client.CollectionExists(context.Background(), collectionName)
|
||||
if err != nil { panic(err) } // @hide
|
||||
if exists {
|
||||
client.DeleteCollection(context.Background(), collectionName)
|
||||
}
|
||||
|
||||
client.CreateCollection(context.Background(), &qdrant.CreateCollection{
|
||||
CollectionName: collectionName,
|
||||
VectorsConfig: qdrant.NewVectorsConfigMap(
|
||||
map[string]*qdrant.VectorParams{
|
||||
"dense": {
|
||||
Size: 384,
|
||||
Distance: qdrant.Distance_Cosine,
|
||||
},
|
||||
"multi": {
|
||||
Size: 96,
|
||||
Distance: qdrant.Distance_Cosine,
|
||||
MultivectorConfig: &qdrant.MultiVectorConfig{
|
||||
Comparator: qdrant.MultiVectorComparator_MaxSim,
|
||||
},
|
||||
HnswConfig: &qdrant.HnswConfigDiff{M: qdrant.PtrOf(uint64(0))}, // Disable HNSW for reranking
|
||||
},
|
||||
},
|
||||
),
|
||||
SparseVectorsConfig: qdrant.NewSparseVectorsConfig(
|
||||
map[string]*qdrant.SparseVectorParams{
|
||||
"sparse": {Modifier: qdrant.Modifier_Idf.Enum()},
|
||||
},
|
||||
),
|
||||
})
|
||||
// @block-end create-collection
|
||||
|
||||
// @block-start ingest-data
|
||||
csvUrl := "https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv"
|
||||
|
||||
batchSize := 25
|
||||
var idx uint64
|
||||
var buffer []*qdrant.PointStruct
|
||||
|
||||
err = parseCSV(csvUrl, func(row CSVRow) {
|
||||
title := row.Title
|
||||
author := row.Author
|
||||
description := row.Description
|
||||
|
||||
buffer = append(buffer, &qdrant.PointStruct{
|
||||
Id: qdrant.NewIDNum(idx),
|
||||
Vectors: qdrant.NewVectorsMap(map[string]*qdrant.Vector{
|
||||
"dense": qdrant.NewVectorDocument(&qdrant.Document{Text: description, Model: denseEmbeddingModel}),
|
||||
"sparse": qdrant.NewVectorDocument(&qdrant.Document{Text: description, Model: sparseEmbeddingModel}),
|
||||
"multi": qdrant.NewVectorDocument(&qdrant.Document{Text: description, Model: lateInteractionEmbeddingModel}),
|
||||
}),
|
||||
Payload: qdrant.NewValueMap(map[string]any{
|
||||
"title": title,
|
||||
"author": author,
|
||||
"description": description,
|
||||
}),
|
||||
})
|
||||
idx++
|
||||
|
||||
if len(buffer) >= batchSize {
|
||||
client.Upsert(context.Background(), &qdrant.UpsertPoints{
|
||||
CollectionName: collectionName,
|
||||
Points: buffer,
|
||||
})
|
||||
buffer = nil
|
||||
}
|
||||
})
|
||||
if err != nil { panic(err) } // @hide
|
||||
|
||||
if len(buffer) > 0 {
|
||||
client.Upsert(context.Background(), &qdrant.UpsertPoints{
|
||||
CollectionName: collectionName,
|
||||
Points: buffer,
|
||||
})
|
||||
}
|
||||
// @block-end ingest-data
|
||||
|
||||
// @block-start dense-retrieval
|
||||
query := "time travel"
|
||||
|
||||
results, err := client.Query(context.Background(), &qdrant.QueryPoints{
|
||||
CollectionName: collectionName,
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: denseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("dense"),
|
||||
Limit: qdrant.PtrOf(uint64(10)),
|
||||
})
|
||||
|
||||
// @hide-start
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// @hide-end
|
||||
|
||||
for _, result := range results {
|
||||
fmt.Println(result)
|
||||
}
|
||||
// @block-end dense-retrieval
|
||||
|
||||
// @block-start sparse-retrieval
|
||||
results, err = client.Query(context.Background(), &qdrant.QueryPoints{
|
||||
CollectionName: collectionName,
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: sparseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("sparse"),
|
||||
Limit: qdrant.PtrOf(uint64(10)),
|
||||
})
|
||||
|
||||
// @hide-start
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// @hide-end
|
||||
|
||||
for _, result := range results {
|
||||
fmt.Println(result)
|
||||
}
|
||||
// @block-end sparse-retrieval
|
||||
|
||||
// @block-start hybrid-search
|
||||
results, err = client.Query(context.Background(), &qdrant.QueryPoints{
|
||||
CollectionName: collectionName,
|
||||
Prefetch: []*qdrant.PrefetchQuery{
|
||||
{
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: denseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("dense"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
{
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: sparseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("sparse"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
},
|
||||
Query: qdrant.NewQueryFusion(qdrant.Fusion_RRF),
|
||||
WithPayload: qdrant.NewWithPayload(true),
|
||||
Limit: qdrant.PtrOf(uint64(10)),
|
||||
})
|
||||
|
||||
// @hide-start
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// @hide-end
|
||||
|
||||
for _, result := range results {
|
||||
fmt.Println(result)
|
||||
}
|
||||
// @block-end hybrid-search
|
||||
|
||||
// @block-start rerank
|
||||
results, err = client.Query(context.Background(), &qdrant.QueryPoints{
|
||||
CollectionName: collectionName,
|
||||
Prefetch: []*qdrant.PrefetchQuery{
|
||||
{
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: denseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("dense"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
{
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: sparseEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("sparse"),
|
||||
Limit: qdrant.PtrOf(uint64(20)),
|
||||
},
|
||||
},
|
||||
Query: qdrant.NewQueryDocument(&qdrant.Document{
|
||||
Text: query,
|
||||
Model: lateInteractionEmbeddingModel,
|
||||
}),
|
||||
Using: qdrant.PtrOf("multi"),
|
||||
WithPayload: qdrant.NewWithPayload(true),
|
||||
Limit: qdrant.PtrOf(uint64(10)),
|
||||
})
|
||||
|
||||
// @hide-start
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// @hide-end
|
||||
|
||||
for _, result := range results {
|
||||
fmt.Println(result)
|
||||
}
|
||||
// @block-end rerank
|
||||
}
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
package com.example.snippets_amalgamation;
|
||||
|
||||
import static io.qdrant.client.QueryFactory.nearest;
|
||||
import static io.qdrant.client.ValueFactory.value;
|
||||
import static io.qdrant.client.VectorFactory.vector;
|
||||
import static io.qdrant.client.VectorsFactory.namedVectors;
|
||||
import static io.qdrant.client.WithPayloadSelectorFactory.enable;
|
||||
|
||||
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.HnswConfigDiff;
|
||||
import io.qdrant.client.grpc.Collections.Modifier;
|
||||
import io.qdrant.client.grpc.Collections.MultiVectorComparator;
|
||||
import io.qdrant.client.grpc.Collections.MultiVectorConfig;
|
||||
import io.qdrant.client.grpc.Collections.SparseVectorConfig;
|
||||
import io.qdrant.client.grpc.Collections.SparseVectorParams;
|
||||
import io.qdrant.client.grpc.Collections.VectorParams;
|
||||
import io.qdrant.client.grpc.Collections.VectorParamsMap;
|
||||
import io.qdrant.client.grpc.Collections.VectorsConfig;
|
||||
import io.qdrant.client.grpc.Points.Document;
|
||||
import io.qdrant.client.grpc.Points.Fusion;
|
||||
import io.qdrant.client.grpc.Points.PointStruct;
|
||||
import io.qdrant.client.grpc.Points.PrefetchQuery;
|
||||
import io.qdrant.client.grpc.Points.Query;
|
||||
import io.qdrant.client.grpc.Points.QueryPoints;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class Snippet {
|
||||
|
||||
// @block-start parse-csv
|
||||
static class CsvRow {
|
||||
final String title;
|
||||
final String author;
|
||||
final String description;
|
||||
CsvRow(String title, String author, String description) {
|
||||
this.title = title; this.author = author; this.description = description;
|
||||
}
|
||||
}
|
||||
|
||||
static Stream<CsvRow> parseCSV(String url) throws Exception {
|
||||
Function<String, List<String>> parseCsvLine = line -> {
|
||||
List<String> fields = new ArrayList<>();
|
||||
boolean inQuotes = false;
|
||||
var sb = new StringBuilder();
|
||||
for (char c : line.toCharArray()) {
|
||||
if (c == '"') {
|
||||
inQuotes = !inQuotes;
|
||||
} else if (c == ',' && !inQuotes) {
|
||||
fields.add(sb.toString());
|
||||
sb.setLength(0);
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
fields.add(sb.toString());
|
||||
return fields;
|
||||
};
|
||||
|
||||
var reader = new BufferedReader(new InputStreamReader(new URL(url).openStream()));
|
||||
String headerLine = reader.readLine();
|
||||
List<String> headers = parseCsvLine.apply(headerLine);
|
||||
int titleIdx = headers.indexOf("Title");
|
||||
int authorIdx = headers.indexOf("Author");
|
||||
int descriptionIdx = headers.indexOf("Description");
|
||||
|
||||
return reader.lines()
|
||||
.map(line -> {
|
||||
List<String> fields = parseCsvLine.apply(line);
|
||||
return new CsvRow(fields.get(titleIdx), fields.get(authorIdx), fields.get(descriptionIdx));
|
||||
})
|
||||
.onClose(() -> { try { reader.close(); } catch (Exception ignored) {} });
|
||||
}
|
||||
// @block-end parse-csv
|
||||
|
||||
public static void run() throws Exception {
|
||||
// @hide-start
|
||||
String QDRANT_URL = "xyz-example.eu-central.aws.cloud.qdrant.io";
|
||||
String QDRANT_API_KEY = "<your-api-key>";
|
||||
// @hide-end
|
||||
// @block-start client-connection
|
||||
QdrantClient client =
|
||||
new QdrantClient(
|
||||
QdrantGrpcClient.newBuilder(QDRANT_URL, 6334, true)
|
||||
.withApiKey(QDRANT_API_KEY)
|
||||
.build());
|
||||
// @block-end client-connection
|
||||
|
||||
// @block-start define-models
|
||||
String denseEmbeddingModel = "sentence-transformers/all-MiniLM-L6-v2";
|
||||
String sparseEmbeddingModel = "qdrant/bm25";
|
||||
String lateInteractionEmbeddingModel = "answerdotai/answerai-colbert-small-v1";
|
||||
// @block-end define-models
|
||||
|
||||
// @block-start create-collection
|
||||
String collectionName = "hybrid-search";
|
||||
|
||||
if (client.collectionExistsAsync(collectionName).get()) {
|
||||
client.deleteCollectionAsync(collectionName).get();
|
||||
}
|
||||
|
||||
client.createCollectionAsync(
|
||||
CreateCollection.newBuilder()
|
||||
.setCollectionName(collectionName)
|
||||
.setVectorsConfig(
|
||||
VectorsConfig.newBuilder()
|
||||
.setParamsMap(
|
||||
VectorParamsMap.newBuilder()
|
||||
.putMap(
|
||||
"dense",
|
||||
VectorParams.newBuilder()
|
||||
.setSize(384)
|
||||
.setDistance(Distance.Cosine)
|
||||
.build())
|
||||
.putMap(
|
||||
"multi",
|
||||
VectorParams.newBuilder()
|
||||
.setSize(96)
|
||||
.setDistance(Distance.Cosine)
|
||||
.setMultivectorConfig(
|
||||
MultiVectorConfig.newBuilder()
|
||||
.setComparator(MultiVectorComparator.MaxSim)
|
||||
.build())
|
||||
.setHnswConfig(
|
||||
HnswConfigDiff.newBuilder()
|
||||
.setM(0) // Disable HNSW for reranking
|
||||
.build())
|
||||
.build())
|
||||
.build()))
|
||||
.setSparseVectorsConfig(
|
||||
SparseVectorConfig.newBuilder()
|
||||
.putMap(
|
||||
"sparse",
|
||||
SparseVectorParams.newBuilder()
|
||||
.setModifier(Modifier.Idf)
|
||||
.build())
|
||||
.build())
|
||||
.build()
|
||||
).get();
|
||||
// @block-end create-collection
|
||||
|
||||
// @block-start ingest-data
|
||||
String csvUrl = "https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv";
|
||||
|
||||
int batchSize = 25;
|
||||
long idx = 0;
|
||||
List<PointStruct> buffer = new ArrayList<>();
|
||||
|
||||
try (var stream = parseCSV(csvUrl)) {
|
||||
for (var row : (Iterable<CsvRow>) stream::iterator) {
|
||||
String title = row.title;
|
||||
String author = row.author;
|
||||
String description = row.description;
|
||||
|
||||
buffer.add(
|
||||
PointStruct.newBuilder()
|
||||
.setId(io.qdrant.client.PointIdFactory.id(idx++))
|
||||
.setVectors(
|
||||
namedVectors(
|
||||
Map.of(
|
||||
"dense",
|
||||
vector(
|
||||
Document.newBuilder()
|
||||
.setText(description)
|
||||
.setModel(denseEmbeddingModel)
|
||||
.build()),
|
||||
"sparse",
|
||||
vector(
|
||||
Document.newBuilder()
|
||||
.setText(description)
|
||||
.setModel(sparseEmbeddingModel)
|
||||
.build()),
|
||||
"multi",
|
||||
vector(
|
||||
Document.newBuilder()
|
||||
.setText(description)
|
||||
.setModel(lateInteractionEmbeddingModel)
|
||||
.build()))))
|
||||
.putAllPayload(
|
||||
Map.of(
|
||||
"title", value(title),
|
||||
"author", value(author),
|
||||
"description", value(description)))
|
||||
.build());
|
||||
|
||||
if (buffer.size() >= batchSize) {
|
||||
client.upsertAsync(collectionName, buffer).get();
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!buffer.isEmpty()) {
|
||||
client.upsertAsync(collectionName, buffer).get();
|
||||
}
|
||||
// @block-end ingest-data
|
||||
|
||||
// @block-start dense-retrieval
|
||||
String query = "time travel";
|
||||
|
||||
var results = client.queryAsync(
|
||||
QueryPoints.newBuilder()
|
||||
.setCollectionName(collectionName)
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(denseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("dense")
|
||||
.setLimit(10)
|
||||
.build()
|
||||
).get();
|
||||
|
||||
for (var result : results) {
|
||||
System.out.println(result);
|
||||
}
|
||||
// @block-end dense-retrieval
|
||||
|
||||
// @block-start sparse-retrieval
|
||||
results = client.queryAsync(
|
||||
QueryPoints.newBuilder()
|
||||
.setCollectionName(collectionName)
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(sparseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("sparse")
|
||||
.setLimit(10)
|
||||
.build()
|
||||
).get();
|
||||
|
||||
for (var result : results) {
|
||||
System.out.println(result);
|
||||
}
|
||||
// @block-end sparse-retrieval
|
||||
|
||||
// @block-start hybrid-search
|
||||
results = client.queryAsync(
|
||||
QueryPoints.newBuilder()
|
||||
.setCollectionName(collectionName)
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(denseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("dense")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(sparseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("sparse")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.setQuery(Query.newBuilder().setFusion(Fusion.RRF).build())
|
||||
.setWithPayload(enable(true))
|
||||
.setLimit(10)
|
||||
.build()
|
||||
).get();
|
||||
|
||||
for (var result : results) {
|
||||
System.out.println(result);
|
||||
}
|
||||
// @block-end hybrid-search
|
||||
|
||||
// @block-start rerank
|
||||
results = client.queryAsync(
|
||||
QueryPoints.newBuilder()
|
||||
.setCollectionName(collectionName)
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(denseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("dense")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.addPrefetch(
|
||||
PrefetchQuery.newBuilder()
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(sparseEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("sparse")
|
||||
.setLimit(20)
|
||||
.build())
|
||||
.setQuery(
|
||||
nearest(
|
||||
Document.newBuilder()
|
||||
.setText(query)
|
||||
.setModel(lateInteractionEmbeddingModel)
|
||||
.build()))
|
||||
.setUsing("multi")
|
||||
.setWithPayload(enable(true))
|
||||
.setLimit(10)
|
||||
.build()
|
||||
).get();
|
||||
|
||||
for (var result : results) {
|
||||
System.out.println(result);
|
||||
}
|
||||
// @block-end rerank
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
# @hide-start
|
||||
# mypy: disable-error-code="arg-type"
|
||||
# @hide-end
|
||||
# @block-start client-connection
|
||||
from qdrant_client import QdrantClient
|
||||
|
||||
client = QdrantClient(
|
||||
url="https://xyz-example.eu-central.aws.cloud.qdrant.io:6333",
|
||||
api_key="<your-api-key>",
|
||||
cloud_inference=True,
|
||||
)
|
||||
# @block-end client-connection
|
||||
|
||||
# @block-start define-models
|
||||
dense_embedding_model = "sentence-transformers/all-MiniLM-L6-v2"
|
||||
sparse_embedding_model = "qdrant/bm25"
|
||||
late_interaction_embedding_model = "answerdotai/answerai-colbert-small-v1"
|
||||
# @block-end define-models
|
||||
|
||||
# @block-start create-collection
|
||||
from qdrant_client.models import Distance, VectorParams, models
|
||||
|
||||
collection_name = "hybrid-search"
|
||||
|
||||
if client.collection_exists(collection_name=collection_name):
|
||||
client.delete_collection(collection_name=collection_name)
|
||||
|
||||
client.create_collection(
|
||||
collection_name,
|
||||
vectors_config={
|
||||
"dense": models.VectorParams(
|
||||
size=384,
|
||||
distance=models.Distance.COSINE,
|
||||
),
|
||||
"multi": models.VectorParams(
|
||||
size=96,
|
||||
distance=models.Distance.COSINE,
|
||||
multivector_config=models.MultiVectorConfig(
|
||||
comparator=models.MultiVectorComparator.MAX_SIM,
|
||||
),
|
||||
hnsw_config=models.HnswConfigDiff(m=0) # Disable HNSW for reranking
|
||||
),
|
||||
},
|
||||
sparse_vectors_config={
|
||||
"sparse": models.SparseVectorParams(modifier=models.Modifier.IDF)
|
||||
}
|
||||
)
|
||||
# @block-end create-collection
|
||||
|
||||
# @block-start parse-csv
|
||||
import csv
|
||||
import urllib.request
|
||||
|
||||
def parse_csv(url):
|
||||
with urllib.request.urlopen(url) as response:
|
||||
reader = csv.DictReader(line.decode('utf-8') for line in response)
|
||||
yield from reader
|
||||
# @block-end parse-csv
|
||||
|
||||
# @block-start ingest-data
|
||||
from qdrant_client.models import Document, PointStruct
|
||||
|
||||
csv_url = 'https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv'
|
||||
|
||||
points = (
|
||||
PointStruct(
|
||||
id=idx,
|
||||
vector={
|
||||
"dense": Document(text=row['Description'], model=dense_embedding_model),
|
||||
"sparse": Document(text=row['Description'], model=sparse_embedding_model),
|
||||
"multi": Document(text=row['Description'], model=late_interaction_embedding_model),
|
||||
},
|
||||
payload={"title": row['Title'], "author": row['Author'], "description": row['Description']}
|
||||
)
|
||||
for idx, row in enumerate(parse_csv(csv_url))
|
||||
)
|
||||
client.upload_points(
|
||||
collection_name=collection_name,
|
||||
points=points,
|
||||
batch_size=25
|
||||
)
|
||||
# @block-end ingest-data
|
||||
|
||||
# @block-start dense-retrieval
|
||||
import pprint
|
||||
|
||||
query = "time travel"
|
||||
|
||||
results = client.query_points(
|
||||
collection_name,
|
||||
query=models.Document(text=query, model=dense_embedding_model),
|
||||
using="dense",
|
||||
limit=10,
|
||||
)
|
||||
|
||||
pprint.pp(results.points)
|
||||
# @block-end dense-retrieval
|
||||
|
||||
# @block-start sparse-retrieval
|
||||
results = client.query_points(
|
||||
collection_name,
|
||||
query=models.Document(text=query, model=sparse_embedding_model),
|
||||
using="sparse",
|
||||
limit=10,
|
||||
)
|
||||
|
||||
pprint.pp(results.points)
|
||||
# @block-end sparse-retrieval
|
||||
|
||||
# @block-start hybrid-search
|
||||
prefetch = [
|
||||
models.Prefetch(
|
||||
query=models.Document(text=query, model=dense_embedding_model),
|
||||
using="dense",
|
||||
limit=20,
|
||||
),
|
||||
models.Prefetch(
|
||||
query=models.Document(text=query, model=sparse_embedding_model),
|
||||
using="sparse",
|
||||
limit=20,
|
||||
),
|
||||
]
|
||||
|
||||
results = client.query_points(
|
||||
collection_name,
|
||||
prefetch=prefetch,
|
||||
query=models.FusionQuery(fusion=models.Fusion.RRF),
|
||||
with_payload=True,
|
||||
limit=10,
|
||||
)
|
||||
|
||||
pprint.pp(results.points)
|
||||
# @block-end hybrid-search
|
||||
|
||||
# @block-start rerank
|
||||
prefetch = [
|
||||
models.Prefetch(
|
||||
query=models.Document(text=query, model=dense_embedding_model),
|
||||
using="dense",
|
||||
limit=20,
|
||||
),
|
||||
models.Prefetch(
|
||||
query=models.Document(text=query, model=sparse_embedding_model),
|
||||
using="sparse",
|
||||
limit=20,
|
||||
),
|
||||
]
|
||||
|
||||
results = client.query_points(
|
||||
collection_name,
|
||||
prefetch=prefetch,
|
||||
query=models.Document(text=query, model=late_interaction_embedding_model),
|
||||
using="multi",
|
||||
with_payload=True,
|
||||
limit=10,
|
||||
)
|
||||
|
||||
pprint.pp(results.points)
|
||||
# @block-end rerank
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
use qdrant_client::Qdrant;
|
||||
use qdrant_client::qdrant::{
|
||||
CreateCollectionBuilder, Distance, Document, Fusion, HnswConfigDiffBuilder,
|
||||
Modifier, MultiVectorComparator, MultiVectorConfigBuilder, NamedVectors, PointStruct,
|
||||
PrefetchQueryBuilder, Query, QueryPointsBuilder, SparseVectorParamsBuilder,
|
||||
SparseVectorsConfigBuilder, UpsertPointsBuilder, VectorParamsBuilder, VectorsConfigBuilder,
|
||||
};
|
||||
|
||||
pub async fn main() -> anyhow::Result<()> {
|
||||
// @hide-start
|
||||
let qdrant_url = "https://xyz-example.eu-central.aws.cloud.qdrant.io:6334";
|
||||
let qdrant_api_key = "<your-api-key>";
|
||||
// @hide-end
|
||||
// @block-start client-connection
|
||||
let client = Qdrant::from_url(qdrant_url)
|
||||
.api_key(qdrant_api_key)
|
||||
.build()?;
|
||||
// @block-end client-connection
|
||||
|
||||
// @block-start define-models
|
||||
let dense_embedding_model = "sentence-transformers/all-MiniLM-L6-v2";
|
||||
let sparse_embedding_model = "qdrant/bm25";
|
||||
let late_interaction_embedding_model = "answerdotai/answerai-colbert-small-v1";
|
||||
// @block-end define-models
|
||||
|
||||
// @block-start create-collection
|
||||
let collection_name = "hybrid-search";
|
||||
|
||||
if client.collection_exists(collection_name).await? {
|
||||
client.delete_collection(collection_name).await?;
|
||||
}
|
||||
|
||||
let mut vectors = VectorsConfigBuilder::default();
|
||||
vectors.add_named_vector_params(
|
||||
"dense",
|
||||
VectorParamsBuilder::new(384, Distance::Cosine),
|
||||
);
|
||||
vectors.add_named_vector_params(
|
||||
"multi",
|
||||
VectorParamsBuilder::new(96, Distance::Cosine)
|
||||
.multivector_config(MultiVectorConfigBuilder::new(MultiVectorComparator::MaxSim))
|
||||
.hnsw_config(HnswConfigDiffBuilder::default().m(0)), // Disable HNSW for reranking
|
||||
);
|
||||
|
||||
let mut sparse = SparseVectorsConfigBuilder::default();
|
||||
sparse.add_named_vector_params(
|
||||
"sparse",
|
||||
SparseVectorParamsBuilder::default().modifier(Modifier::Idf),
|
||||
);
|
||||
|
||||
client
|
||||
.create_collection(
|
||||
CreateCollectionBuilder::new(collection_name)
|
||||
.vectors_config(vectors)
|
||||
.sparse_vectors_config(sparse),
|
||||
)
|
||||
.await?;
|
||||
// @block-end create-collection
|
||||
|
||||
// @block-start parse-csv
|
||||
struct CsvRow {
|
||||
title: String,
|
||||
author: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
fn parse_csv(url: &str) -> anyhow::Result<impl Iterator<Item = anyhow::Result<CsvRow>>> {
|
||||
let reader = ureq::get(url).call()?.into_body().into_reader();
|
||||
let mut rdr = csv::Reader::from_reader(reader);
|
||||
let headers = rdr.headers()?.clone();
|
||||
let title_idx = headers.iter().position(|h| h == "Title").unwrap();
|
||||
let author_idx = headers.iter().position(|h| h == "Author").unwrap();
|
||||
let description_idx = headers.iter().position(|h| h == "Description").unwrap();
|
||||
let iter = rdr.into_records().map(move |result| {
|
||||
let record = result?;
|
||||
Ok(CsvRow {
|
||||
title: record[title_idx].to_string(),
|
||||
author: record[author_idx].to_string(),
|
||||
description: record[description_idx].to_string(),
|
||||
})
|
||||
});
|
||||
Ok(iter)
|
||||
}
|
||||
// @block-end parse-csv
|
||||
|
||||
// @block-start ingest-data
|
||||
let csv_url = "https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv";
|
||||
|
||||
let batch_size = 25;
|
||||
let mut idx: u64 = 0;
|
||||
let mut buffer: Vec<PointStruct> = Vec::new();
|
||||
|
||||
for row in parse_csv(csv_url)? {
|
||||
let row = row?;
|
||||
let title = row.title;
|
||||
let author = row.author;
|
||||
let description = row.description;
|
||||
|
||||
let vectors = NamedVectors::default()
|
||||
.add_vector("dense", Document::new(&description, dense_embedding_model))
|
||||
.add_vector("sparse", Document::new(&description, sparse_embedding_model))
|
||||
.add_vector("multi", Document::new(&description, late_interaction_embedding_model));
|
||||
|
||||
buffer.push(PointStruct::new(
|
||||
idx,
|
||||
vectors,
|
||||
[
|
||||
("title", title.into()),
|
||||
("author", author.into()),
|
||||
("description", description.into()),
|
||||
],
|
||||
));
|
||||
idx += 1;
|
||||
|
||||
if buffer.len() >= batch_size {
|
||||
client
|
||||
.upsert_points(UpsertPointsBuilder::new(
|
||||
collection_name,
|
||||
std::mem::take(&mut buffer),
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if !buffer.is_empty() {
|
||||
client
|
||||
.upsert_points(UpsertPointsBuilder::new(collection_name, buffer))
|
||||
.await?;
|
||||
}
|
||||
// @block-end ingest-data
|
||||
|
||||
// @block-start dense-retrieval
|
||||
let query = "time travel";
|
||||
|
||||
let results = client
|
||||
.query(
|
||||
QueryPointsBuilder::new(collection_name)
|
||||
.query(Query::new_nearest(Document::new(query, dense_embedding_model)))
|
||||
.using("dense")
|
||||
.limit(10),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for result in results.result {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
// @block-end dense-retrieval
|
||||
|
||||
// @block-start sparse-retrieval
|
||||
let results = client
|
||||
.query(
|
||||
QueryPointsBuilder::new(collection_name)
|
||||
.query(Query::new_nearest(Document::new(query, sparse_embedding_model)))
|
||||
.using("sparse")
|
||||
.limit(10),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for result in results.result {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
// @block-end sparse-retrieval
|
||||
|
||||
// @block-start hybrid-search
|
||||
let results = client
|
||||
.query(
|
||||
QueryPointsBuilder::new(collection_name)
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(Document::new(query, dense_embedding_model)))
|
||||
.using("dense")
|
||||
.limit(20u64),
|
||||
)
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(Document::new(query, sparse_embedding_model)))
|
||||
.using("sparse")
|
||||
.limit(20u64),
|
||||
)
|
||||
.query(Query::new_fusion(Fusion::Rrf))
|
||||
.with_payload(true)
|
||||
.limit(10),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for result in results.result {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
// @block-end hybrid-search
|
||||
|
||||
// @block-start rerank
|
||||
let results = client
|
||||
.query(
|
||||
QueryPointsBuilder::new(collection_name)
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(Document::new(query, dense_embedding_model)))
|
||||
.using("dense")
|
||||
.limit(20u64),
|
||||
)
|
||||
.add_prefetch(
|
||||
PrefetchQueryBuilder::default()
|
||||
.query(Query::new_nearest(Document::new(query, sparse_embedding_model)))
|
||||
.using("sparse")
|
||||
.limit(20u64),
|
||||
)
|
||||
.query(Query::new_nearest(Document::new(query, late_interaction_embedding_model)))
|
||||
.using("multi")
|
||||
.with_payload(true)
|
||||
.limit(10),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for result in results.result {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
// @block-end rerank
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
import { QdrantClient, Schemas } from "@qdrant/js-client-rest";
|
||||
|
||||
// @hide-start
|
||||
const QDRANT_URL = "https://xyz-example.eu-central.aws.cloud.qdrant.io";
|
||||
const QDRANT_API_KEY = "<your-api-key>";
|
||||
// @hide-end
|
||||
// @block-start client-connection
|
||||
const client = new QdrantClient({
|
||||
url: QDRANT_URL,
|
||||
apiKey: QDRANT_API_KEY,
|
||||
});
|
||||
// @block-end client-connection
|
||||
|
||||
// @block-start define-models
|
||||
const denseEmbeddingModel = "sentence-transformers/all-MiniLM-L6-v2";
|
||||
const sparseEmbeddingModel = "qdrant/bm25";
|
||||
const lateInteractionEmbeddingModel = "answerdotai/answerai-colbert-small-v1";
|
||||
// @block-end define-models
|
||||
|
||||
// @block-start create-collection
|
||||
const collectionName = "hybrid-search";
|
||||
|
||||
if (await client.collectionExists(collectionName)) {
|
||||
await client.deleteCollection(collectionName);
|
||||
}
|
||||
|
||||
await client.createCollection(collectionName, {
|
||||
vectors: {
|
||||
dense: {
|
||||
size: 384,
|
||||
distance: "Cosine",
|
||||
},
|
||||
multi: {
|
||||
size: 96,
|
||||
distance: "Cosine",
|
||||
multivector_config: { comparator: "max_sim" },
|
||||
hnsw_config: { m: 0 }, // Disable HNSW for reranking
|
||||
},
|
||||
},
|
||||
sparse_vectors: {
|
||||
sparse: { modifier: "idf" },
|
||||
},
|
||||
});
|
||||
// @block-end create-collection
|
||||
|
||||
// @block-start parse-csv
|
||||
function parseCsvLine(line: string): string[] {
|
||||
const fields: string[] = [];
|
||||
let i = 0;
|
||||
while (i < line.length) {
|
||||
if (line[i] === '"') {
|
||||
i++;
|
||||
let field = "";
|
||||
while (i < line.length) {
|
||||
if (line[i] === '"' && line[i + 1] === '"') { field += '"'; i += 2; }
|
||||
else if (line[i] === '"') { i++; break; }
|
||||
else { field += line[i++]; }
|
||||
}
|
||||
fields.push(field);
|
||||
if (line[i] === ",") i++;
|
||||
} else {
|
||||
const start = i;
|
||||
while (i < line.length && line[i] !== ",") i++;
|
||||
fields.push(line.slice(start, i));
|
||||
if (i < line.length) i++;
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
async function* parseCSV(url: string): AsyncGenerator<{ title: string; author: string; description: string }> {
|
||||
const response = await fetch(url);
|
||||
const reader = response.body!.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let remainder = "";
|
||||
let headers: string[] | null = null;
|
||||
let titleIdx = -1;
|
||||
let authorIdx = -1;
|
||||
let descriptionIdx = -1;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
const chunk = done ? "" : decoder.decode(value, { stream: true });
|
||||
const lines = (remainder + chunk).split("\n");
|
||||
remainder = done ? "" : lines.pop()!;
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
if (headers === null) {
|
||||
headers = parseCsvLine(line);
|
||||
titleIdx = headers.indexOf("Title");
|
||||
authorIdx = headers.indexOf("Author");
|
||||
descriptionIdx = headers.indexOf("Description");
|
||||
continue;
|
||||
}
|
||||
const fields = parseCsvLine(line);
|
||||
yield { title: fields[titleIdx], author: fields[authorIdx], description: fields[descriptionIdx] };
|
||||
}
|
||||
|
||||
if (done) break;
|
||||
}
|
||||
}
|
||||
// @block-end parse-csv
|
||||
|
||||
// @block-start ingest-data
|
||||
const csvUrl = "https://raw.githubusercontent.com/qdrant/examples/refs/heads/master/sci-fi-books/top_100_scifi_books_full.csv";
|
||||
|
||||
const batchSize = 25;
|
||||
let idx = 0;
|
||||
let buffer: Schemas["PointStruct"][] = [];
|
||||
|
||||
for await (const { title, author, description } of parseCSV(csvUrl)) {
|
||||
buffer.push({
|
||||
id: idx++,
|
||||
vector: {
|
||||
dense: { text: description, model: denseEmbeddingModel },
|
||||
sparse: { text: description, model: sparseEmbeddingModel },
|
||||
multi: { text: description, model: lateInteractionEmbeddingModel },
|
||||
},
|
||||
payload: { title, author, description },
|
||||
});
|
||||
|
||||
if (buffer.length >= batchSize) {
|
||||
await client.upsert(collectionName, { points: buffer });
|
||||
buffer = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.length > 0) {
|
||||
await client.upsert(collectionName, { points: buffer });
|
||||
}
|
||||
// @block-end ingest-data
|
||||
|
||||
// @block-start dense-retrieval
|
||||
const query = "time travel";
|
||||
|
||||
const denseResults = await client.query(collectionName, {
|
||||
query: { text: query, model: denseEmbeddingModel },
|
||||
using: "dense",
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
console.log(denseResults.points);
|
||||
// @block-end dense-retrieval
|
||||
|
||||
// @block-start sparse-retrieval
|
||||
const sparseResults = await client.query(collectionName, {
|
||||
query: { text: query, model: sparseEmbeddingModel },
|
||||
using: "sparse",
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
console.log(sparseResults.points);
|
||||
// @block-end sparse-retrieval
|
||||
|
||||
// @block-start hybrid-search
|
||||
const hybridResults = await client.query(collectionName, {
|
||||
prefetch: [
|
||||
{
|
||||
query: { text: query, model: denseEmbeddingModel },
|
||||
using: "dense",
|
||||
limit: 20,
|
||||
},
|
||||
{
|
||||
query: { text: query, model: sparseEmbeddingModel },
|
||||
using: "sparse",
|
||||
limit: 20,
|
||||
},
|
||||
],
|
||||
query: { fusion: "rrf" },
|
||||
with_payload: true,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
console.log(hybridResults.points);
|
||||
// @block-end hybrid-search
|
||||
|
||||
// @block-start rerank
|
||||
const rerankedResults = await client.query(collectionName, {
|
||||
prefetch: [
|
||||
{
|
||||
query: { text: query, model: denseEmbeddingModel },
|
||||
using: "dense",
|
||||
limit: 20,
|
||||
},
|
||||
{
|
||||
query: { text: query, model: sparseEmbeddingModel },
|
||||
using: "sparse",
|
||||
limit: 20,
|
||||
},
|
||||
],
|
||||
query: { text: query, model: lateInteractionEmbeddingModel },
|
||||
using: "multi",
|
||||
with_payload: true,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
console.log(rerankedResults.points);
|
||||
// @block-end rerank
|
||||
+96
-247
@@ -11,311 +11,160 @@ aliases:
|
||||
| Time: 40 min | Level: Intermediate |
|
||||
| --- | ----------- |
|
||||
|
||||
Hybrid search combines dense and sparse retrieval to deliver precise and comprehensive results. By adding reranking with ColBERT, you can further refine search outputs for maximum relevance.
|
||||
Reranking is a powerful technique for improving search precision: rather than running an expensive model over your entire corpus, you apply it to a smaller set of candidates already retrieved by a faster method. This keeps latency low while surfacing the most relevant results.
|
||||
|
||||
In this guide, we’ll show you how to implement hybrid search with reranking in Qdrant, leveraging dense, sparse, and late interaction embeddings to create an efficient, high-accuracy search system. Let’s get started!
|
||||
Reranking pairs especially well with [hybrid search](/documentation/search/hybrid-queries/), which casts a wide retrieval net, maximizing recall across several retrieval paths. Reranking can sort the hybrid search results with a deeper relevance signal. A [late interaction model](/course/multi-vector-search/module-1/late-interaction-basics/), for instance, represents both query and document as multiple vectors, enabling more nuanced term-level comparisons than a single embedding can capture.
|
||||
|
||||
In this tutorial, you'll learn how to build a hybrid search engine that uses dense embeddings for semantic search, sparse embeddings for keyword search, and late interaction embeddings for reranking. The result is a powerful search engine that delivers highly relevant results by combining the strengths of different embedding types.
|
||||
|
||||
You'll use [Qdrant Cloud Inference](/documentation/inference/#qdrant-cloud-inference) to generate vector embeddings. The three embedding models used in this tutorial (dense, sparse, and late interaction) are available free of charge on Qdrant Cloud. If you prefer to manage your own embedding infrastructure, you can apply the same principles, but you will need to adapt the code examples to use your embedding service.
|
||||
|
||||
## Overview
|
||||
|
||||
Let’s start by breaking down the architecture:
|
||||
|
||||

|
||||
|
||||
Processing Dense, Sparse, and Late Interaction Embeddings in Vector Databases (VDB)
|
||||
Let's start by breaking down the architecture:
|
||||
|
||||
### Ingestion Stage
|
||||
|
||||
Here’s how we’re going to set up the advanced hybrid search. The process is similar to what we did earlier but with a few powerful additions:
|
||||

|
||||
|
||||
1. **Documents**: Just like before, we start with the raw input—our set of documents that need to be indexed for search.
|
||||
2. **Dense Embeddings**: We’ll generate dense embeddings for each document, just like in the basic search. These embeddings capture the deeper, semantic meanings behind the text.
|
||||
3. **Sparse Embeddings**: This is where it gets interesting. Alongside dense embeddings, we’ll create sparse embeddings using more traditional, keyword-based methods. Specifically, we’ll use BM25, a probabilistic retrieval model. BM25 ranks documents based on how relevant their terms are to a given query, taking into account how often terms appear, document length, and how common the term is across all documents. It’s perfect for keyword-heavy searches.
|
||||
4. **Late Interaction Embeddings**: Now, we add the magic of ColBERT. ColBERT uses a two-stage approach. First, it generates contextualized embeddings for both queries and documents using BERT, and then it performs late interaction—matching those embeddings efficiently using a dot product to fine-tune relevance. This step allows for deeper, contextual understanding, making sure you get the most precise results.
|
||||
5. **Vector Database**: All of these embeddings—dense, sparse, and late interaction—are stored in a vector database like Qdrant. This allows you to efficiently search, retrieve, and rerank your documents based on multiple layers of relevance.
|
||||
You'll start by ingesting a CSV file containing information about science fiction books. Each row is a **document**, corresponding to a book, with fields for the title, author, and description. Each book description will be processed to generate three types of embeddings:
|
||||
- **Dense embeddings** capture the deeper, semantic meanings behind the text.
|
||||
- **Sparse embeddings** support more traditional, keyword-based methods. Specifically, you'll use [BM25](/documentation/search/text-search/#bm25), a probabilistic retrieval model. BM25 ranks documents based on how relevant their terms are to a given query, taking into account how often terms appear, document length, and how common the term is across all documents. It's perfect for keyword-heavy searches.
|
||||
- **Late interaction embeddings** capture the nuanced interactions between query and document terms. You'll use a ColBERT model, which uses a two-stage approach. First, it generates contextualized embeddings for both queries and documents using BERT, and then it performs late interaction, matching those embeddings efficiently to fine-tune relevance. Learn more about late interaction models in the [Multivector Representations for Reranking in Qdrant](/documentation/tutorials-search-engineering/using-multivector-representations/) tutorial and the [Multi-Vector Search](/course/multi-vector-search/) course.
|
||||
|
||||

|
||||
|
||||
Query Retrieval and Reranking Process in Search Systems
|
||||
The data, including all the embeddings, is stored in Qdrant, a **vector search engine**. This enables you to efficiently search, retrieve, and rerank your documents based on multiple layers of relevance.
|
||||
|
||||
### Retrieval Stage
|
||||
|
||||
Now, let's talk about how we’re going to pull the best results once the user submits a query:
|
||||

|
||||
|
||||
1. **User’s Query**: The user enters a query, and that query is transformed into multiple types of embeddings. We’re talking about representations that capture both the deeper meaning (dense) and specific keywords (sparse).
|
||||
2. **Embeddings**: The query gets converted into various embeddings—some for understanding the semantics (dense embeddings) and others for focusing on keyword matches (sparse embeddings).
|
||||
3. **Hybrid Search**: Our hybrid search uses both dense and sparse embeddings to find the most relevant documents. The dense embeddings ensure we capture the overall meaning of the query, while sparse embeddings make sure we don’t miss out on those key, important terms.
|
||||
4. **Rerank**: Once we’ve got a set of documents, the final step is reranking. This is where late interaction embeddings come into play, giving you results that are not only relevant but tuned to your query by prioritizing the documents that truly meet the user's intent.
|
||||
When a user submits a **query**, it is, just like documents, transformed into each of the types of embeddings: dense for semantic search, sparse for keyword search, and late interaction for precise reranking.
|
||||
|
||||
Next, **hybrid search** uses dense and sparse embeddings to find the most relevant documents. The dense embeddings are used for semantic search, while the sparse embeddings are used for keyword search. The resulting sets of documents are then **reranked** using late interaction embeddings, giving results that are not only relevant but also tuned to your query by prioritizing the documents that truly meet the user's intent.
|
||||
|
||||
## Implementation
|
||||
|
||||
Let’s see it in action in this section.
|
||||
### Install and Initialize the Qdrant Client
|
||||
|
||||
### Additional Setup
|
||||
First, install the Qdrant client:
|
||||
|
||||
This time around, we’re using FastEmbed—a lightweight Python library designed for generating embeddings, and it supports popular text models right out of the box. First things first, you’ll need to install it:
|
||||
{{< code-snippet path="/documentation/headless/snippets/install-client/" >}}
|
||||
|
||||
```python
|
||||
pip install fastembed
|
||||
```
|
||||
Next, initialize the client:
|
||||
|
||||
---
|
||||
{{< code-snippet path="/documentation/headless/snippets/tutorial-reranking-hybrid-search/" block="client-connection" >}}
|
||||
|
||||
Here are the models we’ll be pulling from FastEmbed:
|
||||
### Models
|
||||
|
||||
```python
|
||||
from fastembed import TextEmbedding, LateInteractionTextEmbedding, SparseTextEmbedding
|
||||
```
|
||||
Next, define the three embedding models. You'll use the 384-dimensional [`sentence-transformers/all-MiniLM-L6-v2`](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) model for dense embeddings, the [`qdrant/bm25`](https://huggingface.co/Qdrant/bm25) model for sparse embeddings, and the 96-dimensional [`answerdotai/answerai-colbert-small-v1`](https://huggingface.co/answerdotai/answerai-colbert-small-v1) multivector model for late interaction embeddings.
|
||||
|
||||
---
|
||||
|
||||
### Ingestion
|
||||
|
||||
As before, we’ll convert our documents into embeddings, but thanks to FastEmbed, the process is even more straightforward because all the models you need are conveniently available in one location.
|
||||
|
||||
### Embeddings
|
||||
|
||||
First, let’s load the models we need:
|
||||
|
||||
```python
|
||||
dense_embedding_model = TextEmbedding("sentence-transformers/all-MiniLM-L6-v2")
|
||||
bm25_embedding_model = SparseTextEmbedding("Qdrant/bm25")
|
||||
late_interaction_embedding_model = LateInteractionTextEmbedding("colbert-ir/colbertv2.0")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Now, let’s convert our documents into embeddings:
|
||||
|
||||
```python
|
||||
dense_embeddings = list(dense_embedding_model.embed(doc for doc in documents))
|
||||
bm25_embeddings = list(bm25_embedding_model.embed(doc for doc in documents))
|
||||
late_interaction_embeddings = list(late_interaction_embedding_model.embed(doc for doc in documents))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Since we’re dealing with multiple types of embeddings (dense, sparse, and late interaction), we’ll need to store them in a collection that supports a multi-vector setup. The previous collection we created won’t work here, so we’ll create a new one designed specifically for handling these different types of embeddings.
|
||||
{{< code-snippet path="/documentation/headless/snippets/tutorial-reranking-hybrid-search/" block="define-models" >}}
|
||||
|
||||
### Create Collection
|
||||
|
||||
Now, we’re setting up a new collection in Qdrant for our hybrid search with the right configurations to handle all the different vector types we’re working with.
|
||||
Create a new collection called `hybrid-search`, configured to handle the three vector types:
|
||||
|
||||
Here’s how you do it:
|
||||
- **Dense embeddings** (`dense`) using cosine distance for semantic comparisons.
|
||||
- **Late interaction embeddings** (`multi`) using cosine distance, with a multivector configuration using the maximum similarity comparator. Note the `m=0` configuration to disable HNSW indexing. These embeddings are used for reranking, not ANN retrieval, so an HNSW index is not needed.
|
||||
- **Sparse embeddings** (`sparse`) for keyword-based searches using the [IDF modifier](/documentation/manage-data/indexing/#idf-modifier).
|
||||
|
||||
```python
|
||||
from qdrant_client.models import Distance, VectorParams, models
|
||||
{{< code-snippet path="/documentation/headless/snippets/tutorial-reranking-hybrid-search/" block="create-collection" >}}
|
||||
|
||||
client.create_collection(
|
||||
"hybrid-search",
|
||||
vectors_config={
|
||||
"all-MiniLM-L6-v2": models.VectorParams(
|
||||
size=len(dense_embeddings[0]),
|
||||
distance=models.Distance.COSINE,
|
||||
),
|
||||
"colbertv2.0": models.VectorParams(
|
||||
size=len(late_interaction_embeddings[0][0]),
|
||||
distance=models.Distance.COSINE,
|
||||
multivector_config=models.MultiVectorConfig(
|
||||
comparator=models.MultiVectorComparator.MAX_SIM,
|
||||
),
|
||||
hnsw_config=models.HnswConfigDiff(m=0) # Disable HNSW for reranking
|
||||
),
|
||||
},
|
||||
sparse_vectors_config={
|
||||
"bm25": models.SparseVectorParams(modifier=models.Modifier.IDF
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
### Ingest Data
|
||||
|
||||
---
|
||||
Now you can load the sci-fi book descriptions from a CSV and insert them into the `hybrid-search` collection. With Cloud Inference, embeddings are computed server-side by wrapping the text in a `Document` object.
|
||||
|
||||
What’s happening here? We’re creating a collection called "hybrid-search", and we’re configuring it to handle:
|
||||
{{< code-snippet path="/documentation/headless/snippets/tutorial-reranking-hybrid-search/" block="ingest-data" >}}
|
||||
|
||||
- **Dense embeddings** from the model all-MiniLM-L6-v2 using cosine distance for comparisons.
|
||||
- **Late interaction embeddings** from colbertv2.0, also using cosine distance, but with a multivector configuration to use the maximum similarity comparator. Note that we set `m=0` in the `colbertv2.0` vector to prevent indexing since it's not needed for reranking.
|
||||
- **Sparse embeddings** from BM25 for keyword-based searches. They use `dot_product` for similarity calculation.
|
||||
This code creates a point for each book, with three vector types and a payload containing the title, author, and description. Documents are uploaded to Qdrant in batches of 25, with Cloud Inference generating all three embeddings on the fly. In Production, the optimal batch size depends on your data and cluster, so you may want to experiment with different sizes for best performance.
|
||||
|
||||
This setup ensures that all the different types of vectors are stored and compared correctly for your hybrid search.
|
||||
This code uses a helper function to stream and parse the CSV file:
|
||||
|
||||
### Upsert Data
|
||||
|
||||
Next, we need to insert the documents along with their multiple embeddings into the **hybrid-search** collection:
|
||||
|
||||
```python
|
||||
from qdrant_client.models import PointStruct
|
||||
points = []
|
||||
for idx, (dense_embedding, bm25_embedding, late_interaction_embedding, doc) in enumerate(zip(dense_embeddings, bm25_embeddings, late_interaction_embeddings, documents)):
|
||||
|
||||
point = PointStruct(
|
||||
id=idx,
|
||||
vector={
|
||||
"all-MiniLM-L6-v2": dense_embedding,
|
||||
"bm25": bm25_embedding.as_object(),
|
||||
"colbertv2.0": late_interaction_embedding,
|
||||
},
|
||||
payload={"document": doc}
|
||||
)
|
||||
points.append(point)
|
||||
|
||||
operation_info = client.upsert(
|
||||
collection_name="hybrid-search",
|
||||
points=points
|
||||
)
|
||||
```
|
||||
|
||||
<aside role="status">
|
||||
Check how points can be uploaded with builtin Fastembed integration.
|
||||
</aside>
|
||||
|
||||
<details>
|
||||
<summary>Upload with implicit embeddings computation</summary>
|
||||
|
||||
|
||||
```python
|
||||
from qdrant_client.models import PointStruct
|
||||
points = []
|
||||
|
||||
for idx, doc in enumerate(documents):
|
||||
point = PointStruct(
|
||||
id=idx,
|
||||
vector={
|
||||
"all-MiniLM-L6-v2": models.Document(text=doc, model="sentence-transformers/all-MiniLM-L6-v2"),
|
||||
"bm25": models.Document(text=doc, model="Qdrant/bm25"),
|
||||
"colbertv2.0": models.Document(text=doc, model="colbert-ir/colbertv2.0"),
|
||||
},
|
||||
payload={"document": doc}
|
||||
)
|
||||
points.append(point)
|
||||
|
||||
operation_info = client.upsert(
|
||||
collection_name="hybrid-search",
|
||||
points=points
|
||||
)
|
||||
```
|
||||
<details><summary>Details</summary>
|
||||
{{< code-snippet path="/documentation/headless/snippets/time-based-sharding/" block="parse-csv" >}}
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
This code pulls everything together by creating a list of **PointStruct** objects, each containing the embeddings and corresponding documents.
|
||||
|
||||
For each document, it adds:
|
||||
|
||||
- **Dense embeddings** for the deep, semantic meaning.
|
||||
- **BM25 embeddings** for powerful keyword-based search.
|
||||
- **ColBERT embeddings** for precise contextual interactions.
|
||||
|
||||
Once that’s done, the points are uploaded into our **"hybrid-search"** collection using the upsert method, ensuring everything’s in place.
|
||||
|
||||
### Retrieval
|
||||
|
||||
For retrieval, it’s time to convert the user’s query into the required embeddings. Here’s how you can do it:
|
||||
Before combining results, let's see how dense and sparse retrieval perform individually.
|
||||
|
||||
```python
|
||||
dense_vectors = next(dense_embedding_model.query_embed(query))
|
||||
sparse_vectors = next(bm25_embedding_model.query_embed(query))
|
||||
late_vectors = next(late_interaction_embedding_model.query_embed(query))
|
||||
```
|
||||
For retrieval, wrap the query in a `Document` object so Cloud Inference computes the appropriate embeddings server-side.
|
||||
|
||||
---
|
||||
**Dense retrieval** captures semantic meaning:
|
||||
|
||||
The real magic of hybrid search lies in the **prefetch** parameter. This lets you run multiple sub-queries in one go, combining the power of dense and sparse embeddings. Here’s how to set it up, after which we execute the hybrid search:
|
||||
{{< code-snippet path="/documentation/headless/snippets/tutorial-reranking-hybrid-search/" block="dense-retrieval" >}}
|
||||
|
||||
```python
|
||||
prefetch = [
|
||||
models.Prefetch(
|
||||
query=dense_vectors,
|
||||
using="all-MiniLM-L6-v2",
|
||||
limit=20,
|
||||
),
|
||||
models.Prefetch(
|
||||
query=models.SparseVector(**sparse_vectors.as_object()),
|
||||
using="bm25",
|
||||
limit=20,
|
||||
),
|
||||
]
|
||||
```
|
||||
Let's take a look at the top 5 results:
|
||||
|
||||
---
|
||||
| Position | Title | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | The Time Machine | A Victorian scientist travels far into the future to witness civilization's fate. |
|
||||
| 2 | Slaughterhouse-Five | A nonlinear, time-tripping reflection on war and fate. |
|
||||
| 3 | The Peripheral | Two timelines intersect through telepresence technology. |
|
||||
| 4 | The Space Between Worlds | A multiverse traveler uncovers dangerous secrets across parallel Earths. |
|
||||
| 5 | The Forever War | A soldier experiences extreme time dilation while fighting an interstellar war. |
|
||||
|
||||
This code kicks off a hybrid search by running two sub-queries:
|
||||
Each of these books has a strong semantic connection to the concept of time travel, even if the exact phrase doesn't appear in the description.
|
||||
|
||||
- One using dense embeddings from "all-MiniLM-L6-v2" to capture the semantic meaning of the query.
|
||||
- The other using sparse embeddings from BM25 for strong keyword matching.
|
||||
**Sparse retrieval** focuses on keyword matches:
|
||||
|
||||
Each sub-query is limited to 20 results. These sub-queries are bundled together using the prefetch parameter, allowing them to run in parallel.
|
||||
{{< code-snippet path="/documentation/headless/snippets/tutorial-reranking-hybrid-search/" block="sparse-retrieval" >}}
|
||||
|
||||
The top 5 results are:
|
||||
|
||||
| Position | Title | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | Station Eleven | A traveling symphony roams a post-pandemic North America. |
|
||||
| 2 | Hyperion | Travelers share haunting tales on a pilgrimage to confront the mysterious Shrike. |
|
||||
| 3 | The Space Between Worlds | A multiverse traveler uncovers dangerous secrets across parallel Earths. |
|
||||
| 4 | The Time Machine | A Victorian scientist travels far into the future to witness civilization's fate. |
|
||||
| 5 | Slaughterhouse-Five | A nonlinear, time-tripping reflection on war and fate. |
|
||||
|
||||
The sparse BM25 model performs keyword matching with stemming. As a result, it returns books whose descriptions contain variants of the words "time" and "travel". For instance, "Station Eleven" and "Hyperion" mention "traveling" and "travelers" but aren't primarily about time travel.
|
||||
|
||||
**Hybrid search** can be used to prefetch the dense and sparse results and next merge them using [Reciprocal Rank Fusion (RRF)](/documentation/search/hybrid-queries/#reciprocal-rank-fusion-rrf):
|
||||
|
||||
{{< code-snippet path="/documentation/headless/snippets/tutorial-reranking-hybrid-search/" block="hybrid-search" >}}
|
||||
|
||||
This runs two sub-queries in parallel: one using dense embeddings for semantic meaning, the other using sparse BM25 embeddings for keyword matching. The prefetch step retrieves the top 20 candidates from each sub-query (dense and sparse) and fuses the ranked lists into a single result using RRF.
|
||||
|
||||
The results are a mix of books that are semantically relevant to time travel and those that contain the keywords, giving you a broader set of relevant documents. However, the ranking may not be optimal since, [by default, RRF treats both signals equally](/documentation/search/hybrid-queries/#weighted-rrf) and doesn't capture the nuanced interactions between query and document terms. For example, "Station Eleven" ranks highly because it has stronger keyword matches, even though it is not about time travel.
|
||||
|
||||
| Position | Title | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | The Time Machine | A Victorian scientist travels far into the future to witness civilization's fate. |
|
||||
| 2 | Station Eleven | A traveling symphony roams a post-pandemic North America. |
|
||||
| 3 | Slaughterhouse-Five | A nonlinear, time-tripping reflection on war and fate. |
|
||||
| 4 | The Space Between Worlds | A multiverse traveler uncovers dangerous secrets across parallel Earths. |
|
||||
| 5 | Hyperion | Travelers share haunting tales on a pilgrimage to confront the mysterious Shrike. |
|
||||
|
||||
### Rerank
|
||||
|
||||
Now that we've got our initial hybrid search results, it’s time to rerank them using late interaction embeddings for maximum precision. Here’s how you can do it:
|
||||
The hybrid search results can be reranked using late interaction embeddings for maximum precision. Instead of fusing with RRF, use the ColBERT multi-vector as the final ranking signal:
|
||||
|
||||
```python
|
||||
results = client.query_points(
|
||||
"hybrid-search",
|
||||
prefetch=prefetch,
|
||||
query=late_vectors,
|
||||
using="colbertv2.0",
|
||||
with_payload=True,
|
||||
limit=10,
|
||||
)
|
||||
```
|
||||
{{< code-snippet path="/documentation/headless/snippets/tutorial-reranking-hybrid-search/" block="rerank" >}}
|
||||
|
||||
<aside role="status">
|
||||
Check how queries can be made with builtin Fastembed integration.
|
||||
</aside>
|
||||
The prefetch step retrieves the top 20 candidates from each sub-query (dense and sparse), and the ColBERT late interaction model reranks the combined candidates to surface the most relevant results.
|
||||
|
||||
<details>
|
||||
<summary>Query points with implicit embeddings computation</summary>
|
||||
### Compare results
|
||||
|
||||
Let's compare the top 10 results of hybrid search with and without reranking. Notice how some documents shift in rank based on their relevance according to the late interaction embeddings.
|
||||
|
||||
```python
|
||||
prefetch = [
|
||||
models.Prefetch(
|
||||
query=models.Document(text=query, model="sentence-transformers/all-MiniLM-L6-v2"),
|
||||
using="all-MiniLM-L6-v2",
|
||||
limit=20,
|
||||
),
|
||||
models.Prefetch(
|
||||
query=models.Document(text=query, model="Qdrant/bm25"),
|
||||
using="bm25",
|
||||
limit=20,
|
||||
),
|
||||
]
|
||||
results = client.query_points(
|
||||
"hybrid-search",
|
||||
prefetch=prefetch,
|
||||
query=models.Document(text=query, model="colbert-ir/colbertv2.0"),
|
||||
using="colbertv2.0",
|
||||
with_payload=True,
|
||||
limit=10,
|
||||
)
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
---
|
||||
|
||||
Let’s look at how the positions change after applying reranking. Notice how some documents shift in rank based on their relevance according to the late interaction embeddings.
|
||||
|
||||
| | **Document** | **First Query Rank** | **Second Query Rank** | **Rank Change** |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| | In machine learning, feature scaling is the process of normalizing the range of independent variables or features. The goal is to ensure that all features contribute equally to the model, especially in algorithms like SVM or k-nearest neighbors where distance calculations matter. | 1 | 1 | No Change |
|
||||
| | Feature scaling is commonly used in data preprocessing to ensure that features are on the same scale. This is particularly important for gradient descent-based algorithms where features with larger scales could disproportionately impact the cost function. | 2 | 6 | Moved Down |
|
||||
| | Unsupervised learning algorithms, such as clustering methods, may benefit from feature scaling, which ensures that features with larger numerical ranges don't dominate the learning process. | 3 | 4 | Moved Down |
|
||||
| | Data preprocessing steps, including feature scaling, can significantly impact the performance of machine learning models, making it a crucial part of the modeling pipeline. | 5 | 2 | Moved Up |
|
||||
|
||||
Great! We've now explored how reranking works and successfully implemented it.
|
||||
Title | Description | Reranked | RRF rank | Rank Change |
|
||||
|-------|-------------| ---------|----------|-------------|
|
||||
| Slaughterhouse-Five | A nonlinear, time-tripping reflection on war and fate. | 1 | 3 | Moved up |
|
||||
| The Forever War | A soldier experiences extreme time dilation while fighting an interstellar war. | 2 | 8 | Moved up |
|
||||
| Kindred | A modern Black woman is pulled back in time to the antebellum South. | 3 | 7 | Moved up |
|
||||
| Spin | Earth is enclosed in a time-distorting barrier by unknown forces. | 4 | 6 | Moved up |
|
||||
| The Light Brigade | Soldiers are turned into light to fight a war across space-time. | 5 | 10 | Moved up |
|
||||
|
||||
## Best Practices in Reranking
|
||||
|
||||
Reranking can dramatically improve the relevance of search results, especially when combined with hybrid search. Here are some best practices to keep in mind:
|
||||
Reranking with late interaction models can dramatically improve the relevance of search results, especially when combined with hybrid search. Here are some best practices to keep in mind:
|
||||
|
||||
- **Implement Hybrid Reranking**: Blend keyword-based (sparse) and vector-based (dense) search results for a more comprehensive ranking system.
|
||||
- **Continuous Testing and Monitoring**: Regularly evaluate your reranking models to avoid overfitting and make timely adjustments to maintain performance.
|
||||
- **Balance Relevance and Latency**: Reranking can be computationally expensive, so aim for a balance between relevance and speed. Therefore, the first step is to retrieve the relevant documents and then use reranking on it.
|
||||
- **Continuous testing and monitoring**: regularly evaluate your hybrid search pipelines to avoid overfitting and make timely adjustments to maintain performance.
|
||||
- **Balance relevance and cost**: Reranking can be computationally expensive, and late interaction embeddings require significant storage. Aim for a balance between relevance and cost. Simple fusion methods like RRF can be effective for many use cases, while late interaction models can be reserved for queries where precision is critical.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Reranking is a powerful tool that boosts the relevance of search results, especially when combined with hybrid search methods. While it can add some latency due to its complexity, applying it to a smaller, pre-filtered subset of results ensures both speed and relevance.
|
||||
|
||||
Qdrant offers an easy-to-use API to get started with your own search engine, so if you’re ready to dive in, sign up for free at [Qdrant Cloud](https://qdrant.tech/) and start building
|
||||
Reranking with late interaction models is a powerful tool that boosts the relevance of search results, especially when combined with hybrid search methods. While it can add some latency due to its complexity, applying it to a smaller, pre-filtered subset of results ensures both speed and relevance.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Reference in New Issue
Block a user