Skip to content

Commit f0d9f9b

Browse files
Merge branch '106-hnsw-annotation' into 'main'
Add property index annotation for HNSW See merge request objectbox/objectbox-dart!78
2 parents b22ec3c + 889223a commit f0d9f9b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1953
-94
lines changed

dev-doc/updating-c-library.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ For Dart Native and unit tests ([install.sh](../install.sh)),
99
for the binding update script (see below) and
1010
for Flutter (`flutter_libs` and `sync_flutter_libs` plugins) on Linux and Windows:
1111
```
12-
./tool/set-c-version.sh 0.21.0
12+
./tool/set-c-version.sh 4.0.0
1313
```
1414

1515
For the Flutter plugins on Android ([view releases](https://github.com/objectbox/objectbox-java/releases)):
1616
```
17-
./tool/set-android-version.sh 3.8.0
17+
./tool/set-android-version.sh 4.0.0
1818
```
1919

2020
For the Flutter plugins on iOS/macOS ([view releases](https://github.com/objectbox/objectbox-swift/releases))
2121
```
22-
./tool/set-swift-version.sh 1.9.2
22+
./tool/set-swift-version.sh 2.0.0
2323
```
2424

2525
For each, add an entry (see previous releases) to the [CHANGELOG](../objectbox/CHANGELOG.md).

flutter_libs/android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,6 @@ android {
5252
// ObjectBox Android library that includes an ObjectBox C library version compatible with
5353
// the C API binding of the ObjectBox Dart package.
5454
// https://central.sonatype.com/search?q=g:io.objectbox%20objectbox-android
55-
implementation "io.objectbox:objectbox-android:3.8.0"
55+
implementation "io.objectbox:objectbox-android:4.0.0"
5656
}
5757
}

flutter_libs/ios/objectbox_flutter_libs.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Pod::Spec.new do |s|
1818
s.source_files = 'Classes/**/*'
1919

2020
s.dependency 'Flutter'
21-
s.dependency 'ObjectBox', '1.9.2'
21+
s.dependency 'ObjectBox', '2.0.0'
2222

2323
# Flutter.framework does not contain a i386 slice.
2424
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }

flutter_libs/linux/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK)
4444
# ----------------------------------------------------------------------
4545
# Download and add objectbox-c prebuilt library.
4646

47-
set(OBJECTBOX_VERSION 0.21.0)
47+
set(OBJECTBOX_VERSION 4.0.0)
4848

4949
set(OBJECTBOX_ARCH ${CMAKE_SYSTEM_PROCESSOR})
5050
if (${OBJECTBOX_ARCH} MATCHES "x86_64")

flutter_libs/macos/objectbox_flutter_libs.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Pod::Spec.new do |s|
1818
s.source_files = 'Classes/**/*'
1919

2020
s.dependency 'FlutterMacOS'
21-
s.dependency 'ObjectBox', '1.9.2'
21+
s.dependency 'ObjectBox', '2.0.0'
2222

2323
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
2424
s.swift_version = '5.3'

flutter_libs/windows/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ set(objectbox_flutter_libs_bundled_libraries
5050
# ----------------------------------------------------------------------
5151
# Download and add objectbox-c prebuilt library.
5252

53-
set(OBJECTBOX_VERSION 0.21.0)
53+
set(OBJECTBOX_VERSION 4.0.0)
5454

5555
set(OBJECTBOX_ARCH ${CMAKE_SYSTEM_PROCESSOR})
5656
if (${OBJECTBOX_ARCH} MATCHES "AMD64")

generator/lib/src/code_builder.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ class CodeBuilder extends Builder {
211211
propInModel.flags = prop.flags;
212212
propInModel.dartFieldType = prop.dartFieldType;
213213
propInModel.relationTarget = prop.relationTarget;
214+
propInModel.hnswParams = prop.hnswParams;
214215

215216
if (!prop.hasIndexFlag()) {
216217
propInModel.removeIndex();

generator/lib/src/code_chunks.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:build/build.dart';
22
import 'package:collection/collection.dart' show IterableExtension;
33
import 'package:objectbox/internal.dart';
4+
import 'package:objectbox/objectbox.dart';
45
import 'package:pubspec_parse/pubspec_parse.dart';
56
import 'package:source_gen/source_gen.dart' show InvalidGenerationSourceError;
67

@@ -149,6 +150,10 @@ class CodeChunks {
149150
property.relationTarget!.isNotEmpty) {
150151
additionalArgs += ", relationTarget: '${property.relationTarget!}'";
151152
}
153+
if (property.hnswParams != null) {
154+
additionalArgs +=
155+
", hnswParams: ${property.hnswParams!.toCodeString(obxInt)}";
156+
}
152157
return '''
153158
$obxInt.ModelProperty(
154159
id: ${createIdUid(property.id)},
@@ -714,6 +719,8 @@ class CodeChunks {
714719
if (prop.isRelation) {
715720
propCode +=
716721
'$obx.QueryRelationToOne<${entity.name}, ${prop.relationTarget}>';
722+
} else if (prop.hnswParams != null) {
723+
propCode += '$obx.QueryHnswProperty<${entity.name}>';
717724
} else {
718725
propCode += '$obx.Query${fieldType}Property<${entity.name}>';
719726
}

generator/lib/src/entity_resolver.dart

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class EntityResolver extends Builder {
2727
final _uniqueChecker = const TypeChecker.fromRuntime(Unique);
2828
final _indexChecker = const TypeChecker.fromRuntime(Index);
2929
final _backlinkChecker = const TypeChecker.fromRuntime(Backlink);
30+
final _hnswChecker = const TypeChecker.fromRuntime(HnswIndex);
3031

3132
@override
3233
FutureOr<void> build(BuildStep buildStep) async {
@@ -67,6 +68,7 @@ class EntityResolver extends Builder {
6768
null,
6869
uidRequest: !entityUid.isNull && entityUid.intValue == 0);
6970

71+
// Sync: check if enabled
7072
if (_syncChecker.hasAnnotationOfExact(classElement)) {
7173
entity.flags |= OBXEntityFlags.SYNC_ENABLED;
7274
}
@@ -207,6 +209,20 @@ class EntityResolver extends Builder {
207209
processAnnotationIndexUnique(
208210
f, annotated, fieldType, classElement, prop);
209211

212+
// Vector database: check for any HNSW index params
213+
_hnswChecker.runIfMatches(annotated, (annotation) {
214+
// Note: using other index annotations on FloatVector currently
215+
// errors, so no need to integrate with regular index processing.
216+
if (fieldType != OBXPropertyType.FloatVector) {
217+
throw InvalidGenerationSourceError(
218+
"'${classElement.name}.${f.name}': @HnswIndex is only supported for float vector properties.",
219+
element: f);
220+
}
221+
// Create an index
222+
prop.flags |= OBXPropertyFlags.INDEXED;
223+
_readHnswIndexParams(annotation, prop);
224+
});
225+
210226
// for code generation
211227
prop.dartFieldType =
212228
f.type.element!.name! + (isNullable(f.type) ? '?' : '');
@@ -383,8 +399,9 @@ class EntityResolver extends Builder {
383399

384400
// If available use index type from annotation.
385401
if (indexAnnotation != null && !indexAnnotation.isNull) {
386-
final enumValItem = enumValueItem(indexAnnotation.getField('type')!);
387-
if (enumValItem != null) indexType = IndexType.values[enumValItem];
402+
final typeIndex =
403+
_enumValueIndex(indexAnnotation.getField('type')!, "Index.type");
404+
if (typeIndex != null) indexType = IndexType.values[typeIndex];
388405
}
389406

390407
// Fall back to index type based on property type.
@@ -408,10 +425,11 @@ class EntityResolver extends Builder {
408425
if (uniqueAnnotation != null && !uniqueAnnotation.isNull) {
409426
prop.flags |= OBXPropertyFlags.UNIQUE;
410427
// Determine unique conflict resolution.
411-
final onConflictVal =
412-
enumValueItem(uniqueAnnotation.getField('onConflict')!);
413-
if (onConflictVal != null &&
414-
ConflictStrategy.values[onConflictVal] == ConflictStrategy.replace) {
428+
final onConflictIndex = _enumValueIndex(
429+
uniqueAnnotation.getField('onConflict')!, "Unique.onConflict");
430+
if (onConflictIndex != null &&
431+
ConflictStrategy.values[onConflictIndex] ==
432+
ConflictStrategy.replace) {
415433
prop.flags |= OBXPropertyFlags.UNIQUE_ON_CONFLICT_REPLACE;
416434
}
417435
}
@@ -459,28 +477,22 @@ class EntityResolver extends Builder {
459477
}
460478
}
461479

462-
int? enumValueItem(DartObject typeField) {
463-
if (!typeField.isNull) {
464-
final enumValues = (typeField.type as InterfaceType)
465-
.element
466-
.fields
467-
.where((f) => f.isEnumConstant)
468-
.toList();
469-
470-
// Find the index of the matching enum constant.
471-
for (var i = 0; i < enumValues.length; i++) {
472-
if (enumValues[i].computeConstantValue() == typeField) {
473-
return i;
474-
}
475-
}
480+
/// If not null, returns the index of the enum value.
481+
int? _enumValueIndex(DartObject enumState, String fieldName) {
482+
if (enumState.isNull) return null;
483+
// All enum classes implement the Enum interface
484+
// which has the index property.
485+
final index = enumState.getField("index")?.toIntValue();
486+
if (index == null) {
487+
throw ArgumentError.value(enumState, fieldName,
488+
"Dart object state does not appear to represent an enum");
476489
}
477-
478-
return null;
490+
return index;
479491
}
480492

481493
// find out @Property(type:) field value - its an enum PropertyType
482494
int? propertyTypeFromAnnotation(DartObject typeField) {
483-
final item = enumValueItem(typeField);
495+
final item = _enumValueIndex(typeField, "Property.type");
484496
return item == null
485497
? null
486498
: propertyTypeToOBXPropertyType(PropertyType.values[item]);
@@ -522,6 +534,28 @@ class EntityResolver extends Builder {
522534
return info.toString();
523535
}).toList(growable: false);
524536
}
537+
538+
void _readHnswIndexParams(DartObject annotation, ModelProperty property) {
539+
final distanceTypeIndex = _enumValueIndex(
540+
annotation.getField('distanceType')!, "HnswIndex.distanceType");
541+
final distanceType = distanceTypeIndex != null
542+
? VectorDistanceType.values[distanceTypeIndex]
543+
: null;
544+
545+
final hnswRestored = HnswIndex(
546+
dimensions: annotation.getField('dimensions')!.toIntValue()!,
547+
neighborsPerNode: annotation.getField('neighborsPerNode')!.toIntValue(),
548+
indexingSearchCount:
549+
annotation.getField('indexingSearchCount')!.toIntValue(),
550+
flags: _HnswFlagsState.fromState(annotation.getField('flags')!),
551+
distanceType: distanceType,
552+
reparationBacklinkProbability: annotation
553+
.getField('reparationBacklinkProbability')!
554+
.toDoubleValue(),
555+
vectorCacheHintSizeKB:
556+
annotation.getField('vectorCacheHintSizeKB')!.toIntValue());
557+
property.hnswParams = ModelHnswParams.fromAnnotation(hnswRestored);
558+
}
525559
}
526560

527561
extension _TypeCheckerExtensions on TypeChecker {
@@ -530,3 +564,18 @@ extension _TypeCheckerExtensions on TypeChecker {
530564
if (annotations.isNotEmpty) fn(annotations.first);
531565
}
532566
}
567+
568+
extension _HnswFlagsState on HnswFlags {
569+
static HnswFlags? fromState(DartObject state) {
570+
if (state.isNull) return null;
571+
return HnswFlags(
572+
debugLogs: state.getField('debugLogs')!.toBoolValue() ?? false,
573+
debugLogsDetailed:
574+
state.getField('debugLogsDetailed')!.toBoolValue() ?? false,
575+
vectorCacheSimdPaddingOff:
576+
state.getField('vectorCacheSimdPaddingOff')!.toBoolValue() ?? false,
577+
reparationLimitCandidates:
578+
state.getField('reparationLimitCandidates')!.toBoolValue() ??
579+
false);
580+
}
581+
}

generator/test/code_builder_test.dart

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,116 @@ void main() {
242242
List<String>? tStrings;
243243
''');
244244
});
245+
246+
test('HNSW annotation on unsupported type errors', () async {
247+
final source = r'''
248+
library example;
249+
import 'package:objectbox/objectbox.dart';
250+
251+
@Entity()
252+
class Example {
253+
@Id()
254+
int id = 0;
255+
256+
@HnswIndex(dimensions: 3)
257+
List<double>? coordinates;
258+
}
259+
''';
260+
261+
final testEnv = GeneratorTestEnv();
262+
await expectLater(
263+
() async => await testEnv.run(source),
264+
throwsA(isA<InvalidGenerationSourceError>().having(
265+
(e) => e.message,
266+
'message',
267+
contains(
268+
"@HnswIndex is only supported for float vector properties."))));
269+
});
270+
271+
test('HNSW annotation default', () async {
272+
final source = r'''
273+
library example;
274+
import 'package:objectbox/objectbox.dart';
275+
276+
@Entity()
277+
class Example {
278+
@Id()
279+
int id = 0;
280+
281+
@Property(type: PropertyType.floatVector)
282+
@HnswIndex(dimensions: 3)
283+
List<double>? coordinates;
284+
}
285+
''';
286+
287+
final testEnv = GeneratorTestEnv();
288+
await testEnv.run(source);
289+
290+
// Assert final model created by generator
291+
final vectorProperty = testEnv.model.entities[0].properties
292+
.firstWhere((element) => element.name == "coordinates");
293+
expect(vectorProperty.flags & OBXPropertyFlags.INDEXED != 0, true);
294+
expect(vectorProperty.indexId, isNotNull);
295+
expect(vectorProperty.hnswParams, isNotNull);
296+
expect(vectorProperty.hnswParams!.dimensions, 3);
297+
expect(vectorProperty.hnswParams!.neighborsPerNode, isNull);
298+
expect(vectorProperty.hnswParams!.indexingSearchCount, isNull);
299+
expect(vectorProperty.hnswParams!.flags, isNull);
300+
expect(vectorProperty.hnswParams!.distanceType, isNull);
301+
expect(vectorProperty.hnswParams!.reparationBacklinkProbability, isNull);
302+
expect(vectorProperty.hnswParams!.vectorCacheHintSizeKB, isNull);
303+
});
304+
305+
test('HNSW annotation with all properties', () async {
306+
final source = r'''
307+
library example;
308+
import 'package:objectbox/objectbox.dart';
309+
310+
@Entity()
311+
class Example {
312+
@Id()
313+
int id = 0;
314+
315+
@Property(type: PropertyType.floatVector)
316+
@HnswIndex(
317+
dimensions: 3,
318+
neighborsPerNode: 30,
319+
indexingSearchCount: 100,
320+
flags: HnswFlags(
321+
debugLogs: true,
322+
debugLogsDetailed: true,
323+
vectorCacheSimdPaddingOff: true,
324+
reparationLimitCandidates: true),
325+
distanceType: VectorDistanceType.euclidean,
326+
reparationBacklinkProbability: 0.95,
327+
vectorCacheHintSizeKB: 2097152)
328+
List<double>? coordinates;
329+
}
330+
''';
331+
332+
final testEnv = GeneratorTestEnv();
333+
await testEnv.run(source);
334+
335+
// Assert final model created by generator
336+
final vectorProperty = testEnv.model.entities[0].properties
337+
.firstWhere((element) => element.name == "coordinates");
338+
expect(vectorProperty.flags & OBXPropertyFlags.INDEXED != 0, true);
339+
expect(vectorProperty.indexId, isNotNull);
340+
expect(vectorProperty.hnswParams, isNotNull);
341+
expect(vectorProperty.hnswParams!.dimensions, 3);
342+
expect(vectorProperty.hnswParams!.neighborsPerNode, 30);
343+
expect(vectorProperty.hnswParams!.indexingSearchCount, 100);
344+
final flags = vectorProperty.hnswParams!.flags;
345+
expect(flags, isNotNull);
346+
expect(flags! & OBXHnswFlags.DebugLogs != 0, true);
347+
expect(flags & OBXHnswFlags.DebugLogsDetailed != 0, true);
348+
expect(flags & OBXHnswFlags.VectorCacheSimdPaddingOff != 0, true);
349+
expect(flags & OBXHnswFlags.ReparationLimitCandidates != 0, true);
350+
expect(vectorProperty.hnswParams!.distanceType,
351+
OBXVectorDistanceType.Euclidean);
352+
expect(vectorProperty.hnswParams!.reparationBacklinkProbability, 0.95);
353+
expect(vectorProperty.hnswParams!.vectorCacheHintSizeKB, 2097152);
354+
});
245355
});
246356
}
247357

install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ set -eu
55
# It's important that the generated dart bindings and the c-api library version match. Dart won't error on C function
66
# signature mismatch, leading to obscure memory bugs.
77
# For how to upgrade the version see dev-doc/updating-c-library.md
8-
cLibVersion=0.21.0
8+
cLibVersion=4.0.0
99
os=$(uname)
1010
cLibArgs="$*"
1111

0 commit comments

Comments
 (0)