Skip to content

Commit 823c7e1

Browse files
authored
Merge branch 'main' into exclude_vector_source
2 parents f8f1d2c + 15768f6 commit 823c7e1

File tree

9 files changed

+286
-10
lines changed

9 files changed

+286
-10
lines changed

docs/changelog/132845.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 132845
2+
summary: Expose existing DLS cache x-pack usage statistics
3+
area: Authorization
4+
type: enhancement
5+
issues: []

docs/reference/elasticsearch-plugins/plugin-management.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ If Elasticsearch was installed using the deb or rpm package then run `/usr/share
116116

117117
For detailed instructions on installing, managing, and configuring plugins, see the following:
118118

119-
* [Intalling Plugings](./installation.md)
119+
* [Installing Plugins](./installation.md)
120120
* [Custom URL or file system](./plugin-management-custom-url.md)
121121
* [Installing multiple plugins](./installing-multiple-plugins.md)
122122
* [Mandatory plugins](./mandatory-plugins.md)
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
---
2+
navigation_title: Better Binary Quantization (BBQ)
3+
applies_to:
4+
stack: all
5+
serverless: all
6+
---
7+
8+
# Better Binary Quantization (BBQ) [bbq]
9+
10+
Better Binary Quantization (BBQ) is an advanced vector quantization method, designed for large-scale similarity search. BBQ is a form of lossy compression for [`dense_vector` fields](https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector) that enables efficient storage and retrieval of large numbers of vectors, while keeping results close to those from the original uncompressed vectors.
11+
12+
BBQ offers significant improvements over scalar quantization by relying on optimized `bit` level computations to reduce memory usage and computational costs while maintaining high search relevance using pre-computed corrective factors. BBQ is designed to work in combination with [oversampling](#bbq-oversampling) and reranking, and is compatible with various [vector search algorithms](#bbq-vector-search-algorithms), such as [HNSW](#bbq-hnsw) and [brute force (flat)](#bbq-flat).
13+
14+
## How BBQ works [bbq-how-it-works]
15+
16+
BBQ retains the original vector’s dimensionality but transforms the datatype of the dimensions from the original `float32` to `bit` effectively compressing each vector by 32x plus an additional 14 bytes of corrective data per vector. BBQ uses these pre-computed corrective factors as partial distance calculations to help realize impressively robust approximations of the original vector.
17+
18+
Measuring vector similarity with BBQ vectors requires much less computing effort, allowing more candidates to be considered when using the HNSW algorithm. This often results in better ranking quality and improved relevance compared to the original `float32` vectors.
19+
20+
## Supported vector search algorithms [bbq-vector-search-algorithms]
21+
22+
BBQ currently supports two vector search algorithms, each suited to different scenarios. You can configure them by setting the dense vector field’s `index_type`.
23+
24+
### `bbq_hnsw` [bbq-hnsw]
25+
26+
When you set a dense vector field’s `index_options` parameter to `type: bbq_hnsw`, {{es}} uses the HNSW algorithm for fast [kNN search](https://www.elastic.co/docs//solutions/search/vector/knn) on compressed vectors. With the default [oversampling](#bbq-oversampling) applied, it delivers better cost efficiency, lower latency, and improved relevance ranking, making it the best choice for large-scale similarity search.
27+
28+
:::{note}
29+
Starting in version 9.1, `bbq_hnsw` is the default indexing method for new `dense_vector` fields with greater than 384 dimensions, so you typically don’t need to specify it explicitly when creating an index.
30+
31+
Datasets with less than 384 dimensions may see less accuracy and incur a higher overhead cost related to the corrective factors, but we have observed some production datasets perform well even at fairly low dimensions including [tests on e5-small](https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch).
32+
:::
33+
34+
The following example creates an index with a `dense_vector` field configured to use the `bbq_hnsw` algorithm.
35+
36+
```console
37+
PUT bbq_hnsw-index
38+
{
39+
"mappings": {
40+
"properties": {
41+
"my_vector": {
42+
"type": "dense_vector",
43+
"dims": 64,
44+
"index": true,
45+
"index_options": {
46+
"type": "bbq_hnsw"
47+
}
48+
}
49+
}
50+
}
51+
}
52+
```
53+
54+
To change an existing index to use `bbq_hnsw`, update the field mapping:
55+
56+
```console
57+
PUT bbq_hnsw-index/_mapping
58+
{
59+
"properties": {
60+
"my_vector": {
61+
"type": "dense_vector",
62+
"dims": 64,
63+
"index": true,
64+
"index_options": {
65+
"type": "bbq_hnsw"
66+
}
67+
}
68+
}
69+
}
70+
```
71+
72+
After this change, all newly created segments will use the `bbq_hnsw` algorithm. As you add or update documents, the index will gradually convert to `bbq_hnsw`.
73+
74+
To apply `bbq_hnsw` to all vectors at once, reindex them into a new index where the `index_options` parameter's `type` is set to `bbq_hnsw`:
75+
76+
:::::{stepper}
77+
::::{step} Create a destination index
78+
```console
79+
PUT my-index-bbq
80+
{
81+
"mappings": {
82+
"properties": {
83+
"my_vector": {
84+
"type": "dense_vector",
85+
"dims": 64,
86+
"index": true,
87+
"index_options": {
88+
"type": "bbq_hnsw"
89+
}
90+
}
91+
}
92+
}
93+
}
94+
```
95+
::::
96+
97+
::::{step} Reindex the data
98+
```console
99+
POST _reindex
100+
{
101+
"source": { "index": "my-index" }, <1>
102+
"dest": { "index": "my-index-bbq" }
103+
}
104+
```
105+
1. The existing index to be reindexed into the newly created index with the `bbq_hnsw` algorithm.
106+
::::
107+
108+
:::::
109+
110+
### `bbq_flat` [bbq-flat]
111+
112+
When you set a dense vector field’s `index_options` parameter to `type: bbq_flat`, {{es}} uses the BBQ algorithm without HNSW. This option generally requires fewer computing resources and works best when the number of vectors being searched is relatively low.
113+
114+
The following example creates an index with a `dense_vector` field configured to use the `bbq_flat` algorithm.
115+
116+
```console
117+
PUT bbq_flat-index
118+
{
119+
"mappings": {
120+
"properties": {
121+
"my_vector": {
122+
"type": "dense_vector",
123+
"dims": 64,
124+
"index": true,
125+
"index_options": {
126+
"type": "bbq_flat"
127+
}
128+
}
129+
}
130+
}
131+
}
132+
```
133+
134+
## Oversampling [bbq-oversampling]
135+
136+
Oversampling is a technique used with BBQ searches to reduce the accuracy loss from compression. Compression lowers the memory footprint by over 95% and improves query latency, at the cost of decreased result accuracy. This decrease can be mitigated by oversampling during query time and reranking the top results using the full vector.
137+
138+
When you run a kNN search on a BBQ-indexed field, {{es}} automatically retrieves more candidate vectors than the number of results you request. This oversampling improves accuracy by giving the system more vectors to re-rank using their full-precision values before returning the top results.
139+
140+
```console
141+
GET bbq-index/_search
142+
{
143+
"knn": {
144+
"field": "my_vector",
145+
"query_vector": [0.12, -0.45, ...],
146+
"k": 10,
147+
"num_candidates": 100
148+
}
149+
}
150+
```
151+
152+
By default, oversampling is set to 3×, meaning if you request k:10, {{es}} retrieves 30 candidates for re-ranking. You don’t need to configure this behavior; it’s applied automatically for BBQ searches.
153+
154+
:::{note}
155+
You can change oversampling from the default 3× to another value. Refer to [Oversampling and rescoring for quantized vectors](https://www.elastic.co/docs/solutions/search/vector/knn#dense-vector-knn-search-rescoring) for details.
156+
:::
157+
158+
## Learn more [bbq-learn-more]
159+
160+
- [Better Binary Quantization (BBQ) in Lucene and {{es}}](https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch) - Learn how BBQ works, its benefits, and how it reduces memory usage while preserving search accuracy.
161+
- [Dense vector field type](https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector) - Find code examples for using `bbq_hnsw` `index_type`.
162+
- [kNN search](https://www.elastic.co/docs/solutions/search/vector/knn) - Learn about the search algorithm that BBQ works with.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
mapped_pages:
3+
- https://www.elastic.co/guide/en/elasticsearch/reference/current/source-index-settings.html
4+
navigation_title: Source settings
5+
---
6+
7+
# Source index settings [source-index-settings]
8+
9+
All settings around the _source metadata field.
10+
11+
$$$source-mode$$$
12+
13+
`index.source.mode`
14+
: (Static, string) The source mode for the index. Valid values are [`synthetic`](/reference/elasticsearch/mapping-reference/mapping-source-field.md#synthetic-source), [`disabled`](/reference/elasticsearch/mapping-reference/mapping-source-field.md#disable-source-field) or `stored`. Defaults to `stored`. The `stored` source mode always stores the source metadata field on disk.
15+
16+
$$$recovery-use_synthetic_source$$$
17+
18+
`index.recovery.use_synthetic_source`
19+
: (Static, boolean) If synthetic source mode is used, whether the recovery source should also be synthesized instead of stored to disk. Defaults to `true`. This setting can only be configured if synthetic source mode is enabled.
20+
21+
$$$synthetic-source-keep$$$
22+
23+
`index.mapping.synthetic_source_keep`
24+
: (Static, string) Controls how to retain accuracy of fields at the index level. Valid values are `none` or `arrays`.This is a subset of [synthetic source keep mapping attribute](/reference/elasticsearch/mapping-reference/mapping-source-field.md#synthetic-source-keep). Defaults to `arrays` if `index.mode` is `logsdb` or otherwise `none`.

docs/reference/elasticsearch/toc.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ toc:
3737
- file: index-settings/index.md
3838
children:
3939
- file: index-settings/serverless.md
40+
- file: index-settings/bbq.md
4041
- file: index-settings/index-modules.md
4142
- file: index-settings/shard-allocation.md
4243
children:
@@ -56,6 +57,7 @@ toc:
5657
children:
5758
- file: index-settings/preloading-data-into-file-system-cache.md
5859
- file: index-settings/time-series.md
60+
- file: index-settings/source.md
5961
- file: index-settings/translog.md
6062
- file: index-settings/pressure.md
6163
- file: index-settings/path.md
@@ -214,4 +216,4 @@ toc:
214216
- file: command-line-tools/setup-passwords.md
215217
- file: command-line-tools/shard-tool.md
216218
- file: command-line-tools/syskeygen.md
217-
- file: command-line-tools/users-command.md
219+
- file: command-line-tools/users-command.md

muted-tests.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,12 @@ tests:
576576
- class: org.elasticsearch.test.rest.yaml.CcsCommonYamlTestSuiteIT
577577
method: test {p0=search.highlight/50_synthetic_source/text multi fvh source order}
578578
issue: https://github.com/elastic/elasticsearch/issues/133056
579+
- class: org.elasticsearch.upgrades.SyntheticSourceRollingUpgradeIT
580+
method: testIndexing {upgradedNodes=1}
581+
issue: https://github.com/elastic/elasticsearch/issues/133060
582+
- class: org.elasticsearch.upgrades.SyntheticSourceRollingUpgradeIT
583+
method: testIndexing {upgradedNodes=0}
584+
issue: https://github.com/elastic/elasticsearch/issues/133061
579585

580586
# Examples:
581587
#

server/src/main/java/org/elasticsearch/index/mapper/DynamicFieldsBuilder.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -334,11 +334,17 @@ public boolean newDynamicStringField(DocumentParserContext context, String name)
334334
mapperBuilderContext
335335
);
336336
} else {
337+
var indexSettings = context.indexSettings();
337338
return createDynamicField(
338-
new TextFieldMapper.Builder(name, context.indexAnalyzers(), SourceFieldMapper.isSynthetic(context.indexSettings()))
339-
.addMultiField(
340-
new KeywordFieldMapper.Builder("keyword", context.indexSettings().getIndexVersionCreated()).ignoreAbove(256)
341-
),
339+
new TextFieldMapper.Builder(
340+
name,
341+
indexSettings.getIndexVersionCreated(),
342+
context.indexAnalyzers(),
343+
SourceFieldMapper.isSynthetic(indexSettings),
344+
false
345+
).addMultiField(
346+
new KeywordFieldMapper.Builder("keyword", context.indexSettings().getIndexVersionCreated()).ignoreAbove(256)
347+
),
342348
context
343349
);
344350
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/DocumentSubsetBitsetCache.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040

4141
import java.io.Closeable;
4242
import java.io.IOException;
43+
import java.util.Collections;
44+
import java.util.LinkedHashMap;
4345
import java.util.List;
4446
import java.util.Map;
4547
import java.util.Objects;
@@ -320,7 +322,16 @@ public static List<Setting<?>> getSettings() {
320322

321323
public Map<String, Object> usageStats() {
322324
final ByteSizeValue ram = ByteSizeValue.ofBytes(ramBytesUsed());
323-
return Map.of("count", entryCount(), "memory", ram.toString(), "memory_in_bytes", ram.getBytes());
325+
final Cache.Stats cacheStats = bitsetCache.stats();
326+
327+
final Map<String, Object> stats = new LinkedHashMap<>();
328+
stats.put("count", entryCount());
329+
stats.put("memory", ram.toString());
330+
stats.put("memory_in_bytes", ram.getBytes());
331+
stats.put("hits", cacheStats.getHits());
332+
stats.put("misses", cacheStats.getMisses());
333+
stats.put("evictions", cacheStats.getEvictions());
334+
return Collections.unmodifiableMap(stats);
324335
}
325336

326337
private static final class BitsetCacheKey {

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/DocumentSubsetBitsetCacheTests.java

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import java.util.ArrayList;
5454
import java.util.Collections;
5555
import java.util.IdentityHashMap;
56+
import java.util.LinkedHashMap;
5657
import java.util.List;
5758
import java.util.Map;
5859
import java.util.Set;
@@ -62,8 +63,10 @@
6263
import java.util.concurrent.TimeUnit;
6364
import java.util.concurrent.atomic.AtomicInteger;
6465
import java.util.concurrent.atomic.AtomicReference;
66+
import java.util.function.Supplier;
6567

6668
import static org.hamcrest.Matchers.equalTo;
69+
import static org.hamcrest.Matchers.greaterThan;
6770
import static org.hamcrest.Matchers.is;
6871
import static org.hamcrest.Matchers.not;
6972
import static org.hamcrest.Matchers.notNullValue;
@@ -396,9 +399,9 @@ public void testCacheUnderConcurrentAccess() throws Exception {
396399
cache.verifyInternalConsistency();
397400

398401
// Due to cache evictions, we must get more bitsets than fields
399-
assertThat(uniqueBitSets.size(), Matchers.greaterThan(FIELD_COUNT));
402+
assertThat(uniqueBitSets.size(), greaterThan(FIELD_COUNT));
400403
// Due to cache evictions, we must have seen more bitsets than the cache currently holds
401-
assertThat(uniqueBitSets.size(), Matchers.greaterThan(cache.entryCount()));
404+
assertThat(uniqueBitSets.size(), greaterThan(cache.entryCount()));
402405
// Even under concurrent pressure, the cache should hit the expected size
403406
assertThat(cache.entryCount(), is(maxCacheCount));
404407
assertThat(cache.ramBytesUsed(), is(maxCacheBytes));
@@ -517,6 +520,64 @@ public void testEquivalentMatchAllDocsQuery() {
517520
assertFalse(DocumentSubsetBitsetCache.isEffectiveMatchAllDocsQuery(new TermQuery(new Term("term"))));
518521
}
519522

523+
public void testHitsMissesAndEvictionsStats() throws Exception {
524+
// cache that will evict all-but-one element, to test evictions
525+
final long maxCacheBytes = EXPECTED_BYTES_PER_BIT_SET + (EXPECTED_BYTES_PER_BIT_SET / 2);
526+
final Settings settings = Settings.builder()
527+
.put(DocumentSubsetBitsetCache.CACHE_SIZE_SETTING.getKey(), maxCacheBytes + "b")
528+
.build();
529+
final DocumentSubsetBitsetCache cache = newCache(settings);
530+
531+
final Supplier<Map<String, Object>> emptyStatsSupplier = () -> {
532+
final Map<String, Object> stats = new LinkedHashMap<>();
533+
stats.put("count", 0);
534+
stats.put("memory", "0b");
535+
stats.put("memory_in_bytes", 0L);
536+
stats.put("hits", 0L);
537+
stats.put("misses", 0L);
538+
stats.put("evictions", 0L);
539+
return stats;
540+
};
541+
542+
final Map<String, Object> expectedStats = emptyStatsSupplier.get();
543+
assertThat(cache.usageStats(), equalTo(expectedStats));
544+
545+
runTestOnIndex((searchExecutionContext, leafContext) -> {
546+
// first lookup - miss
547+
final Query query1 = QueryBuilders.termQuery("field-1", "value-1").toQuery(searchExecutionContext);
548+
final BitSet bitSet1 = cache.getBitSet(query1, leafContext);
549+
assertThat(bitSet1, notNullValue());
550+
expectedStats.put("count", 1);
551+
expectedStats.put("misses", 1L);
552+
expectedStats.put("memory", EXPECTED_BYTES_PER_BIT_SET + "b");
553+
expectedStats.put("memory_in_bytes", EXPECTED_BYTES_PER_BIT_SET);
554+
assertThat(cache.usageStats(), equalTo(expectedStats));
555+
556+
// second same lookup - hit
557+
final BitSet bitSet1Again = cache.getBitSet(query1, leafContext);
558+
assertThat(bitSet1Again, sameInstance(bitSet1));
559+
expectedStats.put("hits", 1L);
560+
assertThat(cache.usageStats(), equalTo(expectedStats));
561+
562+
// second query - miss, should evict the first one
563+
final Query query2 = QueryBuilders.termQuery("field-2", "value-2").toQuery(searchExecutionContext);
564+
final BitSet bitSet2 = cache.getBitSet(query2, leafContext);
565+
assertThat(bitSet2, notNullValue());
566+
// surprisingly, the eviction callback can call `get` on the cache (asynchronously) which causes another miss (or hit)
567+
// so this assertion is about the current state of the code, rather than the expected or desired state.
568+
// see https://github.com/elastic/elasticsearch/issues/132842
569+
expectedStats.put("misses", 3L);
570+
expectedStats.put("evictions", 1L);
571+
assertBusy(() -> { assertThat(cache.usageStats(), equalTo(expectedStats)); }, 200, TimeUnit.MILLISECONDS);
572+
});
573+
574+
final Map<String, Object> finalStats = emptyStatsSupplier.get();
575+
finalStats.put("hits", 1L);
576+
finalStats.put("misses", 3L);
577+
finalStats.put("evictions", 2L);
578+
assertThat(cache.usageStats(), equalTo(finalStats));
579+
}
580+
520581
private void runTestOnIndex(CheckedBiConsumer<SearchExecutionContext, LeafReaderContext, Exception> body) throws Exception {
521582
runTestOnIndices(1, ctx -> {
522583
final TestIndexContext indexContext = ctx.get(0);
@@ -638,5 +699,4 @@ private void runTestOnIndices(int numberIndices, CheckedConsumer<List<TestIndexC
638699
private DocumentSubsetBitsetCache newCache(Settings settings) {
639700
return new DocumentSubsetBitsetCache(settings, singleThreadExecutor);
640701
}
641-
642702
}

0 commit comments

Comments
 (0)