mirror of
https://github.com/qdrant/landing_page.git
synced 2026-09-25 22:18:30 +02:00
Filtered search benchmarks (#112)
* new: add filtered search bench draft * new: update filtered search benchmark post * new: render article * refactoring: add newlines * new: add filtered search bench file structure, add chart * fix: fix plot preselect * fix: fix canvas id * debug: debug js * debug: add benchmark filter search script to header * fix: fix filter search plot selection * new: update benchmark data * refactoring: remove commented code * fix: fix bench data * new: fix labels order * new: sort dataset labels * new: add some text * fix: fix descriptions * debug: fix mapping not allowed yaml parsing error * new: update bench data * fix: fix dataset name * new: update bench data * fix: fix qdrant kw small vocab * text for the articles * upd text * fix: fix typos * new: replace bars with scatter plots * fix: fix method calls * debug: add initializer * fix: replace debug const with variable * new: remove data based split, remain only one plot * fix: remove geo data for milvus * new: remove range data for weaviate * new: remove int 2048 for milvus, as it crashes at the moment * upd graphs * link --------- Co-authored-by: Andrey Vasnetsov <andrey@vasnetsov.com>
This commit is contained in:
co-authored by
Andrey Vasnetsov
parent
9e0fe47712
commit
faad182279
@@ -6,6 +6,7 @@ keywords:
|
|||||||
- ANN Benchmark
|
- ANN Benchmark
|
||||||
- Qdrant vs Milvus
|
- Qdrant vs Milvus
|
||||||
- Qdrant vs Weaviate
|
- Qdrant vs Weaviate
|
||||||
|
- Qdrant vs Redis
|
||||||
- Qdrant vs ElasticSearch
|
- Qdrant vs ElasticSearch
|
||||||
- benchmark
|
- benchmark
|
||||||
- performance
|
- performance
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ So in our benchmarks, we **focus on the relative numbers**, so it is possible to
|
|||||||
The list will be updated:
|
The list will be updated:
|
||||||
|
|
||||||
* Upload & Search speed on single node - [Benchmark](/benchmarks/single-node-speed-benchmark/)
|
* Upload & Search speed on single node - [Benchmark](/benchmarks/single-node-speed-benchmark/)
|
||||||
|
* Filtered search benchmark - [Benchmark](/benchmarks/#filtered-search-benchmark)
|
||||||
* Memory consumption benchmark - TBD
|
* Memory consumption benchmark - TBD
|
||||||
* Filtered search benchmark - TBD
|
|
||||||
* Cluster mode benchmark - TBD
|
* Cluster mode benchmark - TBD
|
||||||
|
|
||||||
Some of our experiment design decisions are described at [F.A.Q Section](/benchmarks/#benchmarks-faq).
|
Some of our experiment design decisions are described at [F.A.Q Section](/benchmarks/#benchmarks-faq).
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
draft: false
|
||||||
|
id: 5
|
||||||
|
title:
|
||||||
|
description:
|
||||||
|
|
||||||
|
filter_data: /benchmarks/filter-result-2023-02-03.json
|
||||||
|
date: 2023-02-13
|
||||||
|
weight: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Filtered Results
|
||||||
|
|
||||||
|
As you can see from the charts, there are three main patterns:
|
||||||
|
|
||||||
|
- **Speed boost** - for some engines/queries, the filtered search is faster than the unfiltered one. It might happen if the filter is restrictive enough, to completely avoid the usage of the vector index.
|
||||||
|
|
||||||
|
- **Speed downturn** - some engines struggle to keep high RPS, it might be related to the requirement of building a filtering mask for the dataset, as described above.
|
||||||
|
|
||||||
|
- **Accuracy collapse** - some engines are loosing accuracy dramatically under some filters. It is related to the fact that the HNSW graph becomes disconnected, and the search becomes unreliable.
|
||||||
|
|
||||||
|
Qdrant avoids all these problems and also benefits from the speed boost, as it implements an advanced [query planning strategy](/documentation/search/#query-planning).
|
||||||
|
|
||||||
|
<aside role="status">The Filtering Benchmark is all about changes in performance between filter and un-filtered queries. Please refer to the search benchmark for absolute speed comparison.</aside>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
draft: false
|
||||||
|
id: 4
|
||||||
|
title: Filtered search benchmark
|
||||||
|
description:
|
||||||
|
|
||||||
|
date: 2023-02-13
|
||||||
|
weight: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# Filtered search benchmark
|
||||||
|
|
||||||
|
Applying filters to search results brings a whole new level of complexity.
|
||||||
|
It is no longer enough to apply one algorithm to plain data. With filtering, it becomes a matter of the _cross-integration_ of the different indices.
|
||||||
|
|
||||||
|
To measure how well different engines perform in this scenario, we have prepared a set of **Filtered ANN Benchmark Datasets** -
|
||||||
|
https://github.com/qdrant/ann-filtering-benchmark-datasets
|
||||||
|
|
||||||
|
|
||||||
|
It is similar to the ones used in the [ann-benchmarks project](https://github.com/erikbern/ann-benchmarks/) but enriched with payload metadata and pre-generated filtering requests. It includes synthetic and real-world datasets with various filters, from keywords to geo-spatial queries.
|
||||||
|
|
||||||
|
### Why filtering is not trivial?
|
||||||
|
|
||||||
|
Not many ANN algorithms are compatible with filtering.
|
||||||
|
HNSW is one of the few of them, but search engines approach its integration in different ways:
|
||||||
|
|
||||||
|
- Some use **post-filtering**, which applies filters after ANN search. It doesn't scale well as it either loses results or requires many candidates on the first stage.
|
||||||
|
- Others use **pre-filtering**, which requires a binary mask of the whole dataset to be passed into the ANN algorithm. It is also not scalable, as the mask size grows linearly with the dataset size.
|
||||||
|
|
||||||
|
On top of it, there is also a problem with search accuracy.
|
||||||
|
It appears if too many vectors are filtered out, so the HNSW graph becomes disconnected.
|
||||||
|
|
||||||
|
Qdrant uses a different approach, not requiring pre- or post-filtering while addressing the accuracy problem.
|
||||||
|
Read more about the Qdrant approach in our [Filtrable HNSW](/articles/filtrable-hnsw/) article.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if .Params.filter_data }}
|
||||||
|
<div class="row clearfix">
|
||||||
|
<section class="content-side col-lg-12 col-md-12 col-sm-12">
|
||||||
|
{{ partial "benchmark_filter_chart" . }}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
|
||||||
<article class="article article_benchmarks article_narrow">
|
<article class="article article_benchmarks article_narrow">
|
||||||
{{ .Content }}
|
{{ .Content }}
|
||||||
|
|||||||
@@ -12,6 +12,16 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
|
||||||
|
{{ if .Params.filter_data }}
|
||||||
|
<div class="auto-container pt-3 pt-md-5 single-page">
|
||||||
|
<div class="row clearfix">
|
||||||
|
<section class="content-side col-lg-12 col-md-12 col-sm-12">
|
||||||
|
{{ partial "benchmark_filter_chart" . }}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
|
||||||
<div class="auto-container pt-3 pt-md-2 mb-5 single-page">
|
<div class="auto-container pt-3 pt-md-2 mb-5 single-page">
|
||||||
<div class="row clearfix">
|
<div class="row clearfix">
|
||||||
|
|||||||
@@ -110,8 +110,6 @@
|
|||||||
fetch(url)
|
fetch(url)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
console.log(data[0])
|
|
||||||
|
|
||||||
const datasets = getDatasetsList(data);
|
const datasets = getDatasetsList(data);
|
||||||
updataDropdown(datasetSelector, datasets);
|
updataDropdown(datasetSelector, datasets);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<div class="benchmark-wrapper">
|
||||||
|
|
||||||
|
<label for="datasets-selector-{{ .Params.id }}">Dataset:</label>
|
||||||
|
<select name="datasets" id="datasets-selector-{{ .Params.id }}"
|
||||||
|
onchange="renderFilterSelected('{{ .Params.id }}')">
|
||||||
|
|
||||||
|
</select>
|
||||||
|
|
||||||
|
Plot values:
|
||||||
|
|
||||||
|
<!-- Radio button group to select values to render -->
|
||||||
|
<label>
|
||||||
|
<input checked type="radio" name="plot-value-{{ .Params.id }}" value="regular_search"
|
||||||
|
onclick="updateFilterSelected('{{ .Params.id }}', this.value)"/>
|
||||||
|
Regular search
|
||||||
|
</label> |
|
||||||
|
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="plot-value-{{ .Params.id }}" value="filter_search"
|
||||||
|
onclick="updateFilterSelected('{{ .Params.id }}', this.value)"/>
|
||||||
|
Filter search
|
||||||
|
</label> |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<canvas id="chart-{{ .Params.id }}"></canvas>
|
||||||
|
|
||||||
|
<i> Download raw data: <a href="{{ .Params.filter_data }}">here</a> </i>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
let url = "{{ .Params.filter_data }}";
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
type: 'scatter',
|
||||||
|
data: {
|
||||||
|
datasets: []
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'linear',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Precision'
|
||||||
|
},
|
||||||
|
min: -0.1,
|
||||||
|
max: 1.1,
|
||||||
|
ticks: {
|
||||||
|
callback: function (value) {
|
||||||
|
if (value > 1.0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (value < 0.0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'RPS'
|
||||||
|
},
|
||||||
|
min: -30,
|
||||||
|
ticks: {
|
||||||
|
callback: function (value) {
|
||||||
|
return value > 0 ? value.toFixed(0) : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function (tooltipItem) {
|
||||||
|
return [
|
||||||
|
tooltipItem.dataset.label
|
||||||
|
];
|
||||||
|
},
|
||||||
|
title: function (tooltipItem) {
|
||||||
|
return "Precision: " + parseFloat(tooltipItem[0].parsed.x).toFixed(2) + ", RPS: " + parseFloat(tooltipItem[0].parsed.y).toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const chart = new Chart(
|
||||||
|
document.getElementById('chart-{{ .Params.id }}'),
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
let datasetSelector = document.getElementById("datasets-selector-{{ .Params.id }}");
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
// data - raw data from json file, contains _all_ results
|
||||||
|
const datasets = getDatasetsList(data);
|
||||||
|
// datasets - names of the datasets, e.g. "range-100", "range-100-no-filters"
|
||||||
|
let cleanedDatasetsSet = new Set();
|
||||||
|
|
||||||
|
for (let dataset of datasets) {
|
||||||
|
cleanedDatasetsSet.add(dataset.replace('-no-filters', '').replace('-filters', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove "keyword-100" from the list of datasets, as we want to reorder it manually
|
||||||
|
cleanedDatasetsSet.delete("keyword-100");
|
||||||
|
|
||||||
|
let cleanedDatasets = ["keyword-100", ...cleanedDatasetsSet];
|
||||||
|
updataDropdown(datasetSelector, cleanedDatasets);
|
||||||
|
|
||||||
|
window.datasets = {"{{ .Params.id }}": data, ...window.datasets}
|
||||||
|
window.charts = {"{{ .Params.id }}": chart, ...window.charts}
|
||||||
|
|
||||||
|
renderFilterSelected("{{ .Params.id }}");
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -68,6 +68,7 @@
|
|||||||
{{ if eq .Section "benchmarks" }}
|
{{ if eq .Section "benchmarks" }}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<script src="{{ "js/benchmarks.js" | absURL }}"></script>
|
<script src="{{ "js/benchmarks.js" | absURL }}"></script>
|
||||||
|
<script src="{{ "js/benchmarks_filtered_search.js" | absURL }}"></script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
<script src="/js/qdr-scroll.min.js" type="module"></script>
|
<script src="/js/qdr-scroll.min.js" type="module"></script>
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
// const ENGINES = [
|
||||||
|
// "qdrant", "weaviate", "milvus", "redis", "elastic"
|
||||||
|
// ]
|
||||||
|
|
||||||
|
function getFilterSelectedData(chartId) {
|
||||||
|
let data = window.datasets[chartId];
|
||||||
|
let datasetSelector = document.getElementById("datasets-selector-" + chartId);
|
||||||
|
|
||||||
|
let filteredDatasetName = getSelectedValue(datasetSelector) + '-filters';
|
||||||
|
let unFilteredDatasetName = getSelectedValue(datasetSelector) + '-no-filters';
|
||||||
|
|
||||||
|
return {
|
||||||
|
"filtered": filterData(data, {
|
||||||
|
"dataset_name": filteredDatasetName,
|
||||||
|
}),
|
||||||
|
"unfiltered": filterData(data, {
|
||||||
|
"dataset_name": unFilteredDatasetName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilterPlotDataForEngine(data, engine) {
|
||||||
|
let filtered = filterData(data, { "engine_name": engine });
|
||||||
|
let values = [{ "x": 0.0, "y": 0.0 }]
|
||||||
|
if (filtered.length > 0) {
|
||||||
|
values[0]["x"] = filtered[0]['mean_precisions'] || 0.0
|
||||||
|
values[0]["y"] = filtered[0]['rps'] || 0.0
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: engine,
|
||||||
|
data: values,
|
||||||
|
backgroundColor: engineToColor[engine],
|
||||||
|
radius: 10,
|
||||||
|
hoverRadius: 13,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilterPlotData(data) {
|
||||||
|
let plotData = [];
|
||||||
|
|
||||||
|
let all_uniq_engines = new Set(data.map((d) => d.engine_name));
|
||||||
|
// To sorted array
|
||||||
|
all_uniq_engines = [...all_uniq_engines].sort();
|
||||||
|
|
||||||
|
|
||||||
|
for (const engine of all_uniq_engines) {
|
||||||
|
plotData.push(getFilterPlotDataForEngine(data, engine));
|
||||||
|
}
|
||||||
|
|
||||||
|
return plotData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFilterSelected(chartId) {
|
||||||
|
|
||||||
|
// Set default value for radio button
|
||||||
|
let searchTypeSelector = document.getElementsByName("plot-value-" + chartId);
|
||||||
|
searchTypeSelector[0].checked = true;
|
||||||
|
|
||||||
|
|
||||||
|
let chart = window.charts[chartId];
|
||||||
|
let { filtered: filteredData, unfiltered: unFilteredData } = getFilterSelectedData(chartId);
|
||||||
|
/*
|
||||||
|
filteredData = [
|
||||||
|
{
|
||||||
|
"engine_name": "qdrant",
|
||||||
|
"p95_time": 0.010619221997512793,
|
||||||
|
"rps": 1118.8951442866735,
|
||||||
|
"p99_time": 0.024652249003083857,
|
||||||
|
"mean_time": 0.00700474982189371,
|
||||||
|
"mean_precisions": 0.4396960000000001,
|
||||||
|
"engine_params": {
|
||||||
|
"parallel": 8,
|
||||||
|
"search_params": {
|
||||||
|
"hnsw_ef": 128
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"setup_name": "qdrant-m-16-ef-128",
|
||||||
|
"dataset_name": "range-100-filters",
|
||||||
|
"parallel": 8,
|
||||||
|
"upload_time": 44.026487107999856,
|
||||||
|
"total_upload_time": 680.002582219
|
||||||
|
}
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Get max value for y axis, ignore underfined data
|
||||||
|
let maxRPSValue = Math.max(...filteredData.map((d) => d.rps || 0), ...unFilteredData.map((d) => d.rps || 0));
|
||||||
|
|
||||||
|
// Render unfiltered data by default
|
||||||
|
let plotData = getFilterPlotData(unFilteredData);
|
||||||
|
|
||||||
|
chart.options.scales.y.max = (maxRPSValue / 100).toFixed() * 100 + 100;
|
||||||
|
|
||||||
|
renderPlot(chart, plotData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function updatePlot(chart, plotData) {
|
||||||
|
chart.data.datasets.forEach((dataset, idx) => {
|
||||||
|
dataset.data = plotData[idx].data;
|
||||||
|
});
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilterSelected(chartId) {
|
||||||
|
let chart = window.charts[chartId];
|
||||||
|
let { filtered: filteredData, unfiltered: unFilteredData } = getFilterSelectedData(chartId);
|
||||||
|
|
||||||
|
// Based on radio button value, render filtered or unfiltered data
|
||||||
|
let data = [];
|
||||||
|
|
||||||
|
let searchTypeSelector = document.getElementsByName("plot-value-" + chartId);
|
||||||
|
let searchType = getRadioButtonValue(searchTypeSelector);
|
||||||
|
|
||||||
|
if (searchType === "filter_search") {
|
||||||
|
data = filteredData;
|
||||||
|
} else {
|
||||||
|
data = unFilteredData;
|
||||||
|
}
|
||||||
|
|
||||||
|
let plotData = getFilterPlotData(data);
|
||||||
|
|
||||||
|
updatePlot(chart, plotData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user