From 71a71eff7f9829c4bfd84e72864c2712649385d6 Mon Sep 17 00:00:00 2001 From: iberi22 <10615454+iberi22@users.noreply.github.com> Date: Thu, 16 Apr 2026 22:39:22 +0000 Subject: [PATCH] fix: Support multiple vector dimensions in ObjectBoxVectorIndex - Updated `VectorIndex` interface to include `dimension` property. - Refactored `ObjectBoxVectorIndex` to support 384 and 768 dimensions using a factory pattern and dimension-specific entities (`ObxVectorDoc` and `ObxVectorDoc384`). - Updated `MemoryGraph` to automatically use the correct dimension from the `EmbeddingsAdapter`. - Regenerated ObjectBox code to include the new entity. - Added comprehensive tests for multiple vector dimensions. --- .flutter-plugins-dependencies | 1 - analyze_final.txt | 25 ---- analyze_output.txt | 32 ----- lib/objectbox-model.json | 37 ++++- lib/objectbox.g.dart | 108 ++++++++++++++- lib/src/memory_graph.dart | 6 +- lib/src/vector_index.dart | 3 + lib/src/vector_index_objectbox.dart | 201 ++++++++++++++++++++++------ test/vector_dimension_test.dart | 101 ++++++++++++++ 9 files changed, 407 insertions(+), 107 deletions(-) delete mode 100644 .flutter-plugins-dependencies delete mode 100644 analyze_final.txt delete mode 100644 analyze_output.txt create mode 100644 test/vector_dimension_test.dart diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies deleted file mode 100644 index 0ebf3d0..0000000 --- a/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"firebase_core","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\firebase_core-2.32.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_database","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\firebase_database-10.5.7\\\\","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"onnxruntime","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\onnxruntime-1.4.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"firebase_core","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\firebase_core-2.32.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_database","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\firebase_database-10.5.7\\\\","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"onnxruntime","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\onnxruntime-1.4.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"firebase_core","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\firebase_core-2.32.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_database","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\firebase_database-10.5.7\\\\","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"onnxruntime","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\onnxruntime-1.4.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"onnxruntime","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\onnxruntime-1.4.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"firebase_core","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\firebase_core-2.32.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"onnxruntime","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\onnxruntime-1.4.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"firebase_core_web","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\firebase_core_web-2.24.0\\\\","dependencies":[],"dev_dependency":false},{"name":"firebase_database_web","path":"C:\\\\Users\\\\belal\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\firebase_database_web-0.2.5+7\\\\","dependencies":["firebase_core_web"],"dev_dependency":false}]},"dependencyGraph":[{"name":"firebase_core","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","dependencies":[]},{"name":"firebase_database","dependencies":["firebase_core","firebase_database_web"]},{"name":"firebase_database_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"onnxruntime","dependencies":[]}],"date_created":"2025-11-25 14:39:01.438376","version":"3.38.2","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/analyze_final.txt b/analyze_final.txt deleted file mode 100644 index 078f2f0..0000000 --- a/analyze_final.txt +++ /dev/null @@ -1,25 +0,0 @@ -Analyzing isar_agent_memory... - - info - lib\src\hierarchical_graph.dart:3:8 - The import of 'llm_adapter.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - lib\src\models\memory_edge.dart:29:5 - The 'if' statement could be replaced by a null-aware assignment. Try using the '??=' operator to conditionally assign a value. - prefer_conditional_assignment - info - lib\src\models\memory_edge.dart:32:5 - The 'if' statement could be replaced by a null-aware assignment. Try using the '??=' operator to conditionally assign a value. - prefer_conditional_assignment - info - lib\src\models\memory_node.dart:38:5 - The 'if' statement could be replaced by a null-aware assignment. Try using the '??=' operator to conditionally assign a value. - prefer_conditional_assignment - info - lib\src\models\memory_node.dart:41:5 - The 'if' statement could be replaced by a null-aware assignment. Try using the '??=' operator to conditionally assign a value. - prefer_conditional_assignment - info - lib\src\sync\cross_device_sync_manager.dart:3:8 - The import of 'package:isar_agent_memory/src/sync/sync_backend.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - lib\src\sync\cross_device_sync_manager.dart:4:8 - The import of 'package:isar_agent_memory/src/sync/firebase_sync_backend.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - lib\src\sync\cross_device_sync_manager.dart:5:8 - The import of 'package:isar_agent_memory/src/sync/websocket_sync_backend.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - lib\src\sync\cross_device_sync_manager.dart:16:3 - Parameter 'memoryGraph' could be a super parameter. Trying converting 'memoryGraph' to a super parameter. - use_super_parameters - info - lib\src\vector_index_objectbox.dart:3:8 - The import of 'package:objectbox/objectbox.dart' is unnecessary because all of the used elements are also provided by the import of '../objectbox.g.dart'. Try removing the import directive. - unnecessary_import - info - test\advanced_retrieval_test.dart:1:8 - The imported package 'flutter_test' isn't a dependency of the importing package. Try adding a dependency for 'flutter_test' in the 'pubspec.yaml' file. - depend_on_referenced_packages - info - test\cross_device_sync_firebase_test.dart:1:8 - The imported package 'flutter_test' isn't a dependency of the importing package. Try adding a dependency for 'flutter_test' in the 'pubspec.yaml' file. - depend_on_referenced_packages - info - test\cross_device_sync_websocket_test.dart:1:8 - The imported package 'flutter_test' isn't a dependency of the importing package. Try adding a dependency for 'flutter_test' in the 'pubspec.yaml' file. - depend_on_referenced_packages - info - test\hirag_phase2_integration_test.dart:1:8 - The imported package 'flutter_test' isn't a dependency of the importing package. Try adding a dependency for 'flutter_test' in the 'pubspec.yaml' file. - depend_on_referenced_packages - info - test\multi_hop_retrieval_test.dart:1:8 - The imported package 'flutter_test' isn't a dependency of the importing package. Try adding a dependency for 'flutter_test' in the 'pubspec.yaml' file. - depend_on_referenced_packages - info - test\reranking_strategies_test.dart:1:8 - The imported package 'flutter_test' isn't a dependency of the importing package. Try adding a dependency for 'flutter_test' in the 'pubspec.yaml' file. - depend_on_referenced_packages - info - test\reranking_strategies_test.dart:3:8 - The import of 'package:isar_agent_memory/src/rerankers/bm25_reranker.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - test\reranking_strategies_test.dart:4:8 - The import of 'package:isar_agent_memory/src/rerankers/diversity_reranker.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - test\reranking_strategies_test.dart:5:8 - The import of 'package:isar_agent_memory/src/rerankers/mmr_reranker.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - test\reranking_strategies_test.dart:6:8 - The import of 'package:isar_agent_memory/src/rerankers/recency_reranker.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - test\sync_conflict_resolution_test.dart:1:8 - The imported package 'flutter_test' isn't a dependency of the importing package. Try adding a dependency for 'flutter_test' in the 'pubspec.yaml' file. - depend_on_referenced_packages - -21 issues found. diff --git a/analyze_output.txt b/analyze_output.txt deleted file mode 100644 index 5370eaa..0000000 --- a/analyze_output.txt +++ /dev/null @@ -1,32 +0,0 @@ -Analyzing isar_agent_memory... - - error - test\cross_device_sync_websocket_test.dart:8:8 - Target of URI doesn't exist: 'cross_device_sync_websocket_test.mocks.dart'. Try creating the file referenced by the URI, or try using a URI for a file that does exist. - uri_does_not_exist - error - test\cross_device_sync_websocket_test.dart:34:8 - Undefined class 'MockWebSocketChannel'. Try changing the name to the name of an existing class, or creating a class with the name 'MockWebSocketChannel'. - undefined_class - error - test\cross_device_sync_websocket_test.dart:35:8 - Undefined class 'MockWebSocketChannel'. Try changing the name to the name of an existing class, or creating a class with the name 'MockWebSocketChannel'. - undefined_class - error - test\cross_device_sync_websocket_test.dart:63:20 - The function 'MockWebSocketChannel' isn't defined. Try importing the library that defines 'MockWebSocketChannel', correcting the name to the name of an existing function, or defining a function named 'MockWebSocketChannel'. - undefined_function - error - test\cross_device_sync_websocket_test.dart:64:20 - The function 'MockWebSocketChannel' isn't defined. Try importing the library that defines 'MockWebSocketChannel', correcting the name to the name of an existing function, or defining a function named 'MockWebSocketChannel'. - undefined_function - error - test\cross_device_sync_websocket_test.dart:70:40 - The function 'MockWebSocketSink' isn't defined. Try importing the library that defines 'MockWebSocketSink', correcting the name to the name of an existing function, or defining a function named 'MockWebSocketSink'. - undefined_function - error - test\cross_device_sync_websocket_test.dart:71:40 - The function 'MockWebSocketSink' isn't defined. Try importing the library that defines 'MockWebSocketSink', correcting the name to the name of an existing function, or defining a function named 'MockWebSocketSink'. - undefined_function - info - lib\src\hierarchical_graph.dart:3:8 - The import of 'llm_adapter.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - lib\src\models\memory_edge.dart:29:5 - The 'if' statement could be replaced by a null-aware assignment. Try using the '??=' operator to conditionally assign a value. - prefer_conditional_assignment - info - lib\src\models\memory_edge.dart:32:5 - The 'if' statement could be replaced by a null-aware assignment. Try using the '??=' operator to conditionally assign a value. - prefer_conditional_assignment - info - lib\src\models\memory_node.dart:38:5 - The 'if' statement could be replaced by a null-aware assignment. Try using the '??=' operator to conditionally assign a value. - prefer_conditional_assignment - info - lib\src\models\memory_node.dart:41:5 - The 'if' statement could be replaced by a null-aware assignment. Try using the '??=' operator to conditionally assign a value. - prefer_conditional_assignment - info - lib\src\sync\cross_device_sync_manager.dart:3:8 - The import of 'package:isar_agent_memory/src/sync/sync_backend.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - lib\src\sync\cross_device_sync_manager.dart:4:8 - The import of 'package:isar_agent_memory/src/sync/firebase_sync_backend.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - lib\src\sync\cross_device_sync_manager.dart:5:8 - The import of 'package:isar_agent_memory/src/sync/websocket_sync_backend.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - lib\src\sync\cross_device_sync_manager.dart:16:3 - Parameter 'memoryGraph' could be a super parameter. Trying converting 'memoryGraph' to a super parameter. - use_super_parameters - info - lib\src\vector_index_objectbox.dart:3:8 - The import of 'package:objectbox/objectbox.dart' is unnecessary because all of the used elements are also provided by the import of '../objectbox.g.dart'. Try removing the import directive. - unnecessary_import - info - test\advanced_retrieval_test.dart:1:8 - The imported package 'flutter_test' isn't a dependency of the importing package. Try adding a dependency for 'flutter_test' in the 'pubspec.yaml' file. - depend_on_referenced_packages - info - test\cross_device_sync_firebase_test.dart:1:8 - The imported package 'flutter_test' isn't a dependency of the importing package. Try adding a dependency for 'flutter_test' in the 'pubspec.yaml' file. - depend_on_referenced_packages - info - test\cross_device_sync_websocket_test.dart:2:8 - The imported package 'flutter_test' isn't a dependency of the importing package. Try adding a dependency for 'flutter_test' in the 'pubspec.yaml' file. - depend_on_referenced_packages - info - test\hirag_phase2_integration_test.dart:1:8 - The imported package 'flutter_test' isn't a dependency of the importing package. Try adding a dependency for 'flutter_test' in the 'pubspec.yaml' file. - depend_on_referenced_packages - info - test\multi_hop_retrieval_test.dart:1:8 - The imported package 'flutter_test' isn't a dependency of the importing package. Try adding a dependency for 'flutter_test' in the 'pubspec.yaml' file. - depend_on_referenced_packages - info - test\reranking_strategies_test.dart:1:8 - The imported package 'flutter_test' isn't a dependency of the importing package. Try adding a dependency for 'flutter_test' in the 'pubspec.yaml' file. - depend_on_referenced_packages - info - test\reranking_strategies_test.dart:3:8 - The import of 'package:isar_agent_memory/src/rerankers/bm25_reranker.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - test\reranking_strategies_test.dart:4:8 - The import of 'package:isar_agent_memory/src/rerankers/diversity_reranker.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - test\reranking_strategies_test.dart:5:8 - The import of 'package:isar_agent_memory/src/rerankers/mmr_reranker.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - test\reranking_strategies_test.dart:6:8 - The import of 'package:isar_agent_memory/src/rerankers/recency_reranker.dart' is unnecessary because all of the used elements are also provided by the import of 'package:isar_agent_memory/isar_agent_memory.dart'. Try removing the import directive. - unnecessary_import - info - test\sync_conflict_resolution_test.dart:1:8 - The imported package 'flutter_test' isn't a dependency of the importing package. Try adding a dependency for 'flutter_test' in the 'pubspec.yaml' file. - depend_on_referenced_packages - -28 issues found. diff --git a/lib/objectbox-model.json b/lib/objectbox-model.json index 792173b..4b5f3a7 100644 --- a/lib/objectbox-model.json +++ b/lib/objectbox-model.json @@ -35,10 +35,43 @@ } ], "relations": [] + }, + { + "id": "2:2169742263646582653", + "lastPropertyId": "4:1185249969430114870", + "name": "ObxVectorDoc384", + "properties": [ + { + "id": "1:7133141375241171069", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:6827526586863241570", + "name": "docKey", + "indexId": "3:3090916281736609899", + "type": 9, + "flags": 2080 + }, + { + "id": "3:2609265675110662092", + "name": "content", + "type": 9 + }, + { + "id": "4:1185249969430114870", + "name": "vector", + "indexId": "4:6260708321276907025", + "type": 28, + "flags": 8 + } + ], + "relations": [] } ], - "lastEntityId": "1:1718053728221939704", - "lastIndexId": "2:7396986783401268781", + "lastEntityId": "2:2169742263646582653", + "lastIndexId": "4:6260708321276907025", "lastRelationId": "0:0", "lastSequenceId": "0:0", "modelVersion": 5, diff --git a/lib/objectbox.g.dart b/lib/objectbox.g.dart index 0c52867..c640a46 100644 --- a/lib/objectbox.g.dart +++ b/lib/objectbox.g.dart @@ -55,6 +55,43 @@ final _entities = [ relations: [], backlinks: [], ), + obx_int.ModelEntity( + id: const obx_int.IdUid(2, 2169742263646582653), + name: 'ObxVectorDoc384', + lastPropertyId: const obx_int.IdUid(4, 1185249969430114870), + flags: 0, + properties: [ + obx_int.ModelProperty( + id: const obx_int.IdUid(1, 7133141375241171069), + name: 'id', + type: 6, + flags: 1, + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(2, 6827526586863241570), + name: 'docKey', + type: 9, + flags: 2080, + indexId: const obx_int.IdUid(3, 3090916281736609899), + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(3, 2609265675110662092), + name: 'content', + type: 9, + flags: 0, + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(4, 1185249969430114870), + name: 'vector', + type: 28, + flags: 8, + indexId: const obx_int.IdUid(4, 6260708321276907025), + hnswParams: obx_int.ModelHnswParams(dimensions: 384, distanceType: 2), + ), + ], + relations: [], + backlinks: [], + ), ]; /// Shortcut for [obx.Store.new] that passes [getObjectBoxModel] and for Flutter @@ -94,8 +131,8 @@ obx.Store openStore({ obx_int.ModelDefinition getObjectBoxModel() { final model = obx_int.ModelInfo( entities: _entities, - lastEntityId: const obx_int.IdUid(1, 1718053728221939704), - lastIndexId: const obx_int.IdUid(2, 7396986783401268781), + lastEntityId: const obx_int.IdUid(2, 2169742263646582653), + lastIndexId: const obx_int.IdUid(4, 6260708321276907025), lastRelationId: const obx_int.IdUid(0, 0), lastSequenceId: const obx_int.IdUid(0, 0), retiredEntityUids: const [], @@ -149,6 +186,50 @@ obx_int.ModelDefinition getObjectBoxModel() { vector: vectorParam, )..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); + return object; + }, + ), + ObxVectorDoc384: obx_int.EntityDefinition( + model: _entities[1], + toOneRelations: (ObxVectorDoc384 object) => [], + toManyRelations: (ObxVectorDoc384 object) => {}, + getId: (ObxVectorDoc384 object) => object.id, + setId: (ObxVectorDoc384 object, int id) { + object.id = id; + }, + objectToFB: (ObxVectorDoc384 object, fb.Builder fbb) { + final docKeyOffset = fbb.writeString(object.docKey); + final contentOffset = + object.content == null ? null : fbb.writeString(object.content!); + final vectorOffset = + object.vector == null ? null : fbb.writeListFloat32(object.vector!); + fbb.startTable(5); + fbb.addInt64(0, object.id); + fbb.addOffset(1, docKeyOffset); + fbb.addOffset(2, contentOffset); + fbb.addOffset(3, vectorOffset); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (obx.Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + final docKeyParam = const fb.StringReader( + asciiOptimization: true, + ).vTableGet(buffer, rootOffset, 6, ''); + final contentParam = const fb.StringReader( + asciiOptimization: true, + ).vTableGetNullable(buffer, rootOffset, 8); + final vectorParam = const fb.ListReader( + fb.Float32Reader(), + lazy: false, + ).vTableGetNullable(buffer, rootOffset, 10); + final object = ObxVectorDoc384( + docKey: docKeyParam, + content: contentParam, + vector: vectorParam, + )..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); + return object; }, ), @@ -179,3 +260,26 @@ class ObxVectorDoc_ { _entities[0].properties[3], ); } + +/// [ObxVectorDoc384] entity fields to define ObjectBox queries. +class ObxVectorDoc384_ { + /// See [ObxVectorDoc384.id]. + static final id = obx.QueryIntegerProperty( + _entities[1].properties[0], + ); + + /// See [ObxVectorDoc384.docKey]. + static final docKey = obx.QueryStringProperty( + _entities[1].properties[1], + ); + + /// See [ObxVectorDoc384.content]. + static final content = obx.QueryStringProperty( + _entities[1].properties[2], + ); + + /// See [ObxVectorDoc384.vector]. + static final vector = obx.QueryHnswProperty( + _entities[1].properties[3], + ); +} diff --git a/lib/src/memory_graph.dart b/lib/src/memory_graph.dart index 6df94ea..55a82d0 100644 --- a/lib/src/memory_graph.dart +++ b/lib/src/memory_graph.dart @@ -30,7 +30,11 @@ class MemoryGraph { required this.embeddingsAdapter, VectorIndex? index, }) { - _index = index ?? ObjectBoxVectorIndex.open(namespace: 'default'); + _index = index ?? + ObjectBoxVectorIndex.open( + namespace: 'default', + dimension: embeddingsAdapter.dimension, + ); } /// Initializes the vector index with existing nodes from the Isar database. diff --git a/lib/src/vector_index.dart b/lib/src/vector_index.dart index c383de2..66b3d92 100644 --- a/lib/src/vector_index.dart +++ b/lib/src/vector_index.dart @@ -20,6 +20,9 @@ abstract class VectorIndex { /// Human-readable name of the backend (e.g., 'objectbox'). String get provider; + /// The dimension of the vectors supported by this index. + int get dimension; + /// Namespace allows having multiple indices (e.g., by embedding provider + dimension). String get namespace; diff --git a/lib/src/vector_index_objectbox.dart b/lib/src/vector_index_objectbox.dart index 661b51b..914cf16 100644 --- a/lib/src/vector_index_objectbox.dart +++ b/lib/src/vector_index_objectbox.dart @@ -1,52 +1,63 @@ import 'dart:math' as math; import 'dart:typed_data'; +import 'package:objectbox/objectbox.dart'; import '../objectbox.g.dart'; import 'vector_index.dart'; -/// ObjectBox entity to store vectors with an HNSW index. -/// NOTE: For now we fix the embedding dimension to 768 (Gemini text-embedding-004). -/// If you need a different dimension, create a separate entity and index. +/// Interface for ObjectBox vector entities to allow generic indexing logic. +abstract class ObxVectorEntity { + int get id; + set id(int value); + String get docKey; + String? get content; + List? get vector; +} + +/// ObjectBox entity to store vectors with an HNSW index (768 dims). @Entity() -class ObxVectorDoc { +class ObxVectorDoc implements ObxVectorEntity { @Id() + @override int id = 0; - /// Namespaced ID (e.g., "default:123"). @Unique() + @override String docKey; + @override String? content; - /// Float vector with HNSW index. The dimension must match your embeddings. - /// We default to 768 (Gemini text-embedding-004), but ObjectBox requires - /// fixed dimensions at compile time. If you need 1536 (OpenAI), change this - /// and regenerate code. @HnswIndex(dimensions: 768, distanceType: VectorDistanceType.cosine) @Property(type: PropertyType.floatVector) + @override List? vector; ObxVectorDoc({required this.docKey, this.content, this.vector}); } -class ObjectBoxVectorIndex implements VectorIndex { - final String _namespace; - final bool _normalize; - final VectorMetric _metric; - final Store _store; - late final Box _box; +/// ObjectBox entity to store vectors with an HNSW index (384 dims). +@Entity() +class ObxVectorDoc384 implements ObxVectorEntity { + @Id() + @override + int id = 0; - ObjectBoxVectorIndex({ - required Store store, - String namespace = 'default', - bool normalize = true, - VectorMetric metric = VectorMetric.cosine, - }) : _store = store, - _namespace = namespace, - _normalize = normalize, - _metric = metric { - _box = Box(_store); - } + @Unique() + @override + String docKey; + + @override + String? content; + + @HnswIndex(dimensions: 384, distanceType: VectorDistanceType.cosine) + @Property(type: PropertyType.floatVector) + @override + List? vector; + ObxVectorDoc384({required this.docKey, this.content, this.vector}); +} + +abstract class ObjectBoxVectorIndex implements VectorIndex { /// Convenience: open a Store internally and create an index. /// Consumers don't need to import generated code. static ObjectBoxVectorIndex open({ @@ -54,6 +65,7 @@ class ObjectBoxVectorIndex implements VectorIndex { String namespace = 'default', bool normalize = true, VectorMetric metric = VectorMetric.cosine, + int dimension = 768, }) { final store = openStore(directory: directory); return ObjectBoxVectorIndex( @@ -61,12 +73,68 @@ class ObjectBoxVectorIndex implements VectorIndex { namespace: namespace, normalize: normalize, metric: metric, + dimension: dimension, ); } + factory ObjectBoxVectorIndex({ + required Store store, + String namespace = 'default', + bool normalize = true, + VectorMetric metric = VectorMetric.cosine, + int dimension = 768, + }) { + if (dimension == 384) { + return _ObjectBoxVectorIndex384( + store: store, + namespace: namespace, + normalize: normalize, + metric: metric, + ); + } else if (dimension == 768) { + return _ObjectBoxVectorIndex768( + store: store, + namespace: namespace, + normalize: normalize, + metric: metric, + ); + } else { + throw ArgumentError( + 'ObjectBoxVectorIndex currently only supports dimensions 384 and 768. ' + 'Requested: $dimension. You may need to add a new entity and update the factory.'); + } + } +} + +abstract class _BaseObjectBoxVectorIndex + implements ObjectBoxVectorIndex { + final String _namespace; + final bool _normalize; + final VectorMetric _metric; + final Store _store; + final int _dimension; + late final Box _box; + + _BaseObjectBoxVectorIndex({ + required Store store, + required int dimension, + String namespace = 'default', + bool normalize = true, + VectorMetric metric = VectorMetric.cosine, + }) : _store = store, + _dimension = dimension, + _namespace = namespace, + _normalize = normalize, + _metric = metric { + _box = Box(_store); + } + @override String get provider => 'objectbox'; + @override + int get dimension => _dimension; + @override String get namespace => _namespace; @@ -92,18 +160,23 @@ class ObjectBoxVectorIndex implements VectorIndex { String _key(String id) => '$_namespace:$id'; + /// Returns the query property for docKey. + QueryStringProperty get _docKeyProperty; + + /// Returns the query property for the vector. + QueryHnswProperty get _vectorProperty; + + /// Factory to create a new entity instance. + T _createEntity(String key, String content, List vector); + @override Future addDocument( String id, String content, Float32List vector) async { // Check dimension - if (vector.length != 768) { - // Warning or throw? For now we throw to avoid crashes deep in ObjectBox - // or silent failures. - // Ideally, we should support multiple dimensions, but ObjectBox requires - // fixed dimensions per @HnswIndex. + if (vector.length != _dimension) { throw ArgumentError( - 'ObjectBoxVectorIndex requires vectors of dimension 768. ' - 'Received ${vector.length}. Change the @HnswIndex annotation and regenerate if needed.', + 'ObjectBoxVectorIndex($_dimension) requires vectors of dimension $_dimension. ' + 'Received ${vector.length}.', ); } @@ -115,12 +188,8 @@ class ObjectBoxVectorIndex implements VectorIndex { final key = _key(id); // Upsert by unique docKey final existing = - _box.query(ObxVectorDoc_.docKey.equals(key)).build().findFirst(); - final entity = ObxVectorDoc( - docKey: key, - content: content, - vector: vec.toList(growable: false), - ); + _box.query(_docKeyProperty.equals(key)).build().findFirst(); + final entity = _createEntity(key, content, vec.toList(growable: false)); if (existing != null) { entity.id = existing.id; } @@ -130,7 +199,7 @@ class ObjectBoxVectorIndex implements VectorIndex { @override Future removeDocument(String id) async { final key = _key(id); - final qb = _box.query(ObxVectorDoc_.docKey.equals(key)).build(); + final qb = _box.query(_docKeyProperty.equals(key)).build(); final found = qb.findFirst(); qb.close(); if (found != null) { @@ -141,9 +210,9 @@ class ObjectBoxVectorIndex implements VectorIndex { @override Future> search(Float32List query, {int topK = 5}) async { - if (query.length != 768) { + if (query.length != _dimension) { throw ArgumentError( - 'ObjectBoxVectorIndex requires query vectors of dimension 768. ' + 'ObjectBoxVectorIndex($_dimension) requires query vectors of dimension $_dimension. ' 'Received ${query.length}.', ); } @@ -154,8 +223,8 @@ class ObjectBoxVectorIndex implements VectorIndex { } final qb = _box - .query(ObxVectorDoc_.vector - .nearestNeighborsF32(q.toList(growable: false), topK)) + .query(_vectorProperty.nearestNeighborsF32( + q.toList(growable: false), topK)) .build(); try { final results = qb.findWithScores(); @@ -180,3 +249,47 @@ class ObjectBoxVectorIndex implements VectorIndex { // ObjectBox is persisted automatically; nothing to load explicitly. } } + +class _ObjectBoxVectorIndex768 extends _BaseObjectBoxVectorIndex { + _ObjectBoxVectorIndex768({ + required super.store, + super.namespace, + super.normalize, + super.metric, + }) : super(dimension: 768); + + @override + QueryStringProperty get _docKeyProperty => ObxVectorDoc_.docKey; + + @override + QueryHnswProperty get _vectorProperty => ObxVectorDoc_.vector; + + @override + ObxVectorDoc _createEntity(String key, String content, List vector) { + return ObxVectorDoc(docKey: key, content: content, vector: vector); + } +} + +class _ObjectBoxVectorIndex384 + extends _BaseObjectBoxVectorIndex { + _ObjectBoxVectorIndex384({ + required super.store, + super.namespace, + super.normalize, + super.metric, + }) : super(dimension: 384); + + @override + QueryStringProperty get _docKeyProperty => + ObxVectorDoc384_.docKey; + + @override + QueryHnswProperty get _vectorProperty => + ObxVectorDoc384_.vector; + + @override + ObxVectorDoc384 _createEntity( + String key, String content, List vector) { + return ObxVectorDoc384(docKey: key, content: content, vector: vector); + } +} diff --git a/test/vector_dimension_test.dart b/test/vector_dimension_test.dart new file mode 100644 index 0000000..15eb351 --- /dev/null +++ b/test/vector_dimension_test.dart @@ -0,0 +1,101 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'package:isar/isar.dart'; +import 'package:isar_agent_memory/isar_agent_memory.dart'; +import 'package:isar_agent_memory/src/vector_index_objectbox.dart'; +import 'package:isar_agent_memory/objectbox.g.dart'; +import 'package:test/test.dart'; +import 'package:path/path.dart' as path; + +class MockEmbeddingsAdapter implements EmbeddingsAdapter { + @override + final int dimension; + @override + final String providerName = 'mock'; + + MockEmbeddingsAdapter(this.dimension); + + @override + Future> embed(String text) async { + return List.generate(dimension, (i) => i.toDouble()); + } +} + +void main() { + late Isar isar; + final tempDir = Directory.systemTemp.createTempSync('isar_test'); + + setUpAll(() async { + await Isar.initializeIsarCore(download: true); + isar = await Isar.open( + [MemoryNodeSchema, MemoryEdgeSchema], + directory: tempDir.path, + ); + }); + + tearDownAll(() async { + await isar.close(); + if (tempDir.existsSync()) { + tempDir.deleteSync(recursive: true); + } + }); + + group('Vector Dimension Compatibility', () { + test('MemoryGraph initializes with 768-dim adapter (default)', () async { + final adapter = MockEmbeddingsAdapter(768); + final obxDir = path.join(tempDir.path, 'obx_768'); + + final index = ObjectBoxVectorIndex.open( + directory: obxDir, + dimension: 768, + ); + + final graph = MemoryGraph(isar, embeddingsAdapter: adapter, index: index); + await graph.initialize(); + + expect(index.dimension, equals(768)); + + final nodeId = await graph.storeNodeWithEmbedding(content: 'test 768'); + expect(nodeId, isNotNull); + + final results = + await graph.semanticSearch(await adapter.embed('test 768')); + expect(results, isNotEmpty); + expect(results.first.node.content, equals('test 768')); + }); + + test('MemoryGraph initializes with 384-dim adapter', () async { + final adapter = MockEmbeddingsAdapter(384); + final obxDir = path.join(tempDir.path, 'obx_384'); + + // MemoryGraph should automatically pick 384 if we don't provide index + // But for testing multiple in one run we might need different directories + final index = ObjectBoxVectorIndex.open( + directory: obxDir, + dimension: 384, + ); + + final graph = MemoryGraph(isar, embeddingsAdapter: adapter, index: index); + await graph.initialize(); + + expect(index.dimension, equals(384)); + + final nodeId = await graph.storeNodeWithEmbedding(content: 'test 384'); + expect(nodeId, isNotNull); + + final results = + await graph.semanticSearch(await adapter.embed('test 384')); + expect(results, isNotEmpty); + expect(results.first.node.content, equals('test 384')); + }); + + test('ObjectBoxVectorIndex throws on unsupported dimension', () { + final store = openStore(directory: path.join(tempDir.path, 'obx_fail')); + expect( + () => ObjectBoxVectorIndex(store: store, dimension: 512), + throwsArgumentError, + ); + store.close(); + }); + }); +}