Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Isar with SQLite for storing CLIP embeddings #1575

Merged
merged 15 commits into from
May 2, 2024
1 change: 1 addition & 0 deletions .github/workflows/mobile-internal-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ jobs:
packageName: io.ente.photos
releaseFiles: mobile/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab
track: internal
changesNotSentForReview: true
6 changes: 0 additions & 6 deletions mobile/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,6 @@ PODS:
- FlutterMacOS
- integration_test (0.0.1):
- Flutter
- isar_flutter_libs (1.0.0):
- Flutter
- libwebp (1.3.2):
- libwebp/demux (= 1.3.2)
- libwebp/mux (= 1.3.2)
Expand Down Expand Up @@ -246,7 +244,6 @@ DEPENDENCIES:
- image_editor_common (from `.symlinks/plugins/image_editor_common/ios`)
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
- media_extension (from `.symlinks/plugins/media_extension/ios`)
Expand Down Expand Up @@ -341,8 +338,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
isar_flutter_libs:
:path: ".symlinks/plugins/isar_flutter_libs/ios"
local_auth_darwin:
:path: ".symlinks/plugins/local_auth_darwin/darwin"
local_auth_ios:
Expand Down Expand Up @@ -427,7 +422,6 @@ SPEC CHECKSUMS:
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
in_app_purchase_storekit: 0e4b3c2e43ba1e1281f4f46dd71b0593ce529892
integration_test: 13825b8a9334a850581300559b8839134b124670
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
Expand Down
2 changes: 0 additions & 2 deletions mobile/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,6 @@
"${BUILT_PRODUCTS_DIR}/image_editor_common/image_editor_common.framework",
"${BUILT_PRODUCTS_DIR}/in_app_purchase_storekit/in_app_purchase_storekit.framework",
"${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework",
"${BUILT_PRODUCTS_DIR}/isar_flutter_libs/isar_flutter_libs.framework",
"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
"${BUILT_PRODUCTS_DIR}/local_auth_darwin/local_auth_darwin.framework",
"${BUILT_PRODUCTS_DIR}/local_auth_ios/local_auth_ios.framework",
Expand Down Expand Up @@ -390,7 +389,6 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_editor_common.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase_storekit.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/isar_flutter_libs.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_darwin.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_ios.framework",
Expand Down
174 changes: 131 additions & 43 deletions mobile/lib/db/embeddings_db.dart
Original file line number Diff line number Diff line change
@@ -1,79 +1,167 @@
import "dart:io";
import "dart:typed_data";

import "package:isar/isar.dart";
import "package:path/path.dart";
import 'package:path_provider/path_provider.dart';
import "package:photos/core/event_bus.dart";
import "package:photos/events/embedding_updated_event.dart";
import "package:photos/models/embedding.dart";
import "package:sqlite_async/sqlite_async.dart";

class EmbeddingsDB {
late final Isar _isar;

EmbeddingsDB._privateConstructor();

static final EmbeddingsDB instance = EmbeddingsDB._privateConstructor();

static const databaseName = "ente.embeddings.db";
static const tableName = "embeddings";
static const columnFileID = "file_id";
static const columnModel = "model";
static const columnEmbedding = "embedding";
static const columnUpdationTime = "updation_time";

static Future<SqliteDatabase>? _dbFuture;

Future<SqliteDatabase> get _database async {
_dbFuture ??= _initDatabase();
return _dbFuture!;
}

Future<void> init() async {
final dir = await getApplicationDocumentsDirectory();
_isar = await Isar.open(
[EmbeddingSchema],
directory: dir.path,
);
await _clearDeprecatedStore(dir);
await _clearDeprecatedStores(dir);
}

Future<SqliteDatabase> _initDatabase() async {
final Directory documentsDirectory =
await getApplicationDocumentsDirectory();
final String path = join(documentsDirectory.path, databaseName);
final migrations = SqliteMigrations()
..add(
SqliteMigration(
1,
(tx) async {
await tx.execute(
'CREATE TABLE $tableName ($columnFileID INTEGER NOT NULL, $columnModel INTEGER NOT NULL, $columnEmbedding BLOB NOT NULL, $columnUpdationTime INTEGER, UNIQUE ($columnFileID, $columnModel))',
);
},
),
);
final database = SqliteDatabase(path: path);
await migrations.migrate(database);
return database;
}

Future<void> clearTable() async {
await _isar.writeTxn(() => _isar.clear());
final db = await _database;
await db.execute('DELETE * FROM $tableName');
}

Future<List<Embedding>> getAll(Model model) async {
return _isar.embeddings.filter().modelEqualTo(model).findAll();
final db = await _database;
final results = await db.getAll('SELECT * FROM $tableName');
return _convertToEmbeddings(results);
}

Future<void> put(Embedding embedding) {
return _isar.writeTxn(() async {
await _isar.embeddings.putByIndex(Embedding.index, embedding);
Bus.instance.fire(EmbeddingUpdatedEvent());
});
Future<void> put(Embedding embedding) async {
final db = await _database;
await db.execute(
'INSERT OR REPLACE INTO $tableName ($columnFileID, $columnModel, $columnEmbedding, $columnUpdationTime) VALUES (?, ?, ?, ?)',
_getRowFromEmbedding(embedding),
);
Bus.instance.fire(EmbeddingUpdatedEvent());
}

Future<void> putMany(List<Embedding> embeddings) {
return _isar.writeTxn(() async {
await _isar.embeddings.putAllByIndex(Embedding.index, embeddings);
Bus.instance.fire(EmbeddingUpdatedEvent());
});
Future<void> putMany(List<Embedding> embeddings) async {
final db = await _database;
final inputs = embeddings.map((e) => _getRowFromEmbedding(e)).toList();
await db.executeBatch(
'INSERT OR REPLACE INTO $tableName ($columnFileID, $columnModel, $columnEmbedding, $columnUpdationTime) values(?, ?, ?, ?)',
inputs,
);
Bus.instance.fire(EmbeddingUpdatedEvent());
}

Future<List<Embedding>> getUnsyncedEmbeddings() async {
return await _isar.embeddings.filter().updationTimeEqualTo(null).findAll();
final db = await _database;
final results = await db.getAll(
'SELECT * FROM $tableName WHERE $columnUpdationTime IS NULL',
);
return _convertToEmbeddings(results);
}

Future<void> deleteEmbeddings(List<int> fileIDs) async {
await _isar.writeTxn(() async {
final embeddings = <Embedding>[];
for (final fileID in fileIDs) {
embeddings.addAll(
await _isar.embeddings.filter().fileIDEqualTo(fileID).findAll(),
);
}
await _isar.embeddings.deleteAll(embeddings.map((e) => e.id).toList());
Bus.instance.fire(EmbeddingUpdatedEvent());
});
final db = await _database;
await db.execute(
'DELETE FROM $tableName WHERE $columnFileID IN (${fileIDs.join(", ")})',
);
Bus.instance.fire(EmbeddingUpdatedEvent());
}

Future<void> deleteAllForModel(Model model) async {
await _isar.writeTxn(() async {
final embeddings =
await _isar.embeddings.filter().modelEqualTo(model).findAll();
await _isar.embeddings.deleteAll(embeddings.map((e) => e.id).toList());
Bus.instance.fire(EmbeddingUpdatedEvent());
});
}

Future<void> _clearDeprecatedStore(Directory dir) async {
final deprecatedStore = Directory(dir.path + "/object-box-store");
if (await deprecatedStore.exists()) {
await deprecatedStore.delete(recursive: true);
final db = await _database;
await db.execute(
'DELETE FROM $tableName WHERE $columnModel = ?',
[modelToInt(model)!],
);
Bus.instance.fire(EmbeddingUpdatedEvent());
}

List<Embedding> _convertToEmbeddings(List<Map<String, dynamic>> results) {
final List<Embedding> embeddings = [];
for (final result in results) {
embeddings.add(_getEmbeddingFromRow(result));
}
return embeddings;
}

Embedding _getEmbeddingFromRow(Map<String, dynamic> row) {
final fileID = row[columnFileID];
final model = intToModel(row[columnModel])!;
final bytes = row[columnEmbedding] as Uint8List;
final list = Float32List.view(bytes.buffer);
return Embedding(fileID: fileID, model: model, embedding: list);
}

List<Object?> _getRowFromEmbedding(Embedding embedding) {
return [
embedding.fileID,
modelToInt(embedding.model)!,
Float32List.fromList(embedding.embedding).buffer.asUint8List(),
embedding.updationTime,
];
}

Future<void> _clearDeprecatedStores(Directory dir) async {
final deprecatedObjectBox = Directory(dir.path + "/object-box-store");
if (await deprecatedObjectBox.exists()) {
await deprecatedObjectBox.delete(recursive: true);
}
final deprecatedIsar = File(dir.path + "/default.isar");
if (await deprecatedIsar.exists()) {
await deprecatedIsar.delete();
}
}

int? modelToInt(Model model) {
switch (model) {
case Model.onnxClip:
return 1;
case Model.ggmlClip:
return 2;
default:
return null;
}
}

Model? intToModel(int model) {
switch (model) {
case 1:
return Model.onnxClip;
case 2:
return Model.ggmlClip;
default:
return null;
}
}
}
10 changes: 0 additions & 10 deletions mobile/lib/models/embedding.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
import "dart:convert";

import "package:isar/isar.dart";

part 'embedding.g.dart';

@collection
class Embedding {
static const index = 'unique_file_model_embedding';

Id id = Isar.autoIncrement;
final int fileID;
@enumerated
@Index(name: index, composite: [CompositeIndex('fileID')], unique: true, replace: true)
final Model model;
final List<double> embedding;
int? updationTime;
Expand Down