diff --git a/TakeNote.xcodeproj/project.pbxproj b/TakeNote.xcodeproj/project.pbxproj index 38eb16c..3b2ba0a 100644 --- a/TakeNote.xcodeproj/project.pbxproj +++ b/TakeNote.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 6602CB572E683E54005A4D38 /* ObjectBox.xcframework in Frameworks */ = {isa = PBXBuildFile; productRef = 6602CB562E683E54005A4D38 /* ObjectBox.xcframework */; }; + 6602CB632E6AF792005A4D38 /* EntityInfo-TakeNote.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6602CB622E6AF792005A4D38 /* EntityInfo-TakeNote.generated.swift */; }; 66AC246F2E4609FB009B0C3F /* CodeEditorView in Frameworks */ = {isa = PBXBuildFile; productRef = 66AC246E2E4609FB009B0C3F /* CodeEditorView */; }; 66AC24712E4609FB009B0C3F /* LanguageSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 66AC24702E4609FB009B0C3F /* LanguageSupport */; }; 66B616ED2E4CC2B6009A1857 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = 66B616EC2E4CC2B6009A1857 /* SQLite */; }; @@ -14,6 +16,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 6602CB622E6AF792005A4D38 /* EntityInfo-TakeNote.generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "EntityInfo-TakeNote.generated.swift"; path = "generated/EntityInfo-TakeNote.generated.swift"; sourceTree = ""; }; 66B6907A2E3FCDF20013519B /* TakeNote.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TakeNote.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -47,6 +50,7 @@ buildActionMask = 2147483647; files = ( 66B616ED2E4CC2B6009A1857 /* SQLite in Frameworks */, + 6602CB572E683E54005A4D38 /* ObjectBox.xcframework in Frameworks */, 66EEDE112E3FFBDD00A899AE /* MarkdownUI in Frameworks */, 66AC24712E4609FB009B0C3F /* LanguageSupport in Frameworks */, 66AC246F2E4609FB009B0C3F /* CodeEditorView in Frameworks */, @@ -59,6 +63,7 @@ 66B690712E3FCDF20013519B = { isa = PBXGroup; children = ( + 6602CB622E6AF792005A4D38 /* EntityInfo-TakeNote.generated.swift */, 66B6907C2E3FCDF20013519B /* TakeNote */, 66B6907B2E3FCDF20013519B /* Products */, ); @@ -96,6 +101,7 @@ 66AC246E2E4609FB009B0C3F /* CodeEditorView */, 66AC24702E4609FB009B0C3F /* LanguageSupport */, 66B616EC2E4CC2B6009A1857 /* SQLite */, + 6602CB562E683E54005A4D38 /* ObjectBox.xcframework */, ); productName = TakeNote; productReference = 66B6907A2E3FCDF20013519B /* TakeNote.app */; @@ -129,6 +135,7 @@ 66EEDE0F2E3FFBDD00A899AE /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, 66AC246D2E4609FB009B0C3F /* XCRemoteSwiftPackageReference "CodeEditorView" */, 66B616EB2E4CC2B6009A1857 /* XCRemoteSwiftPackageReference "SQLite" */, + 6602CB552E683E54005A4D38 /* XCRemoteSwiftPackageReference "objectbox-swift-spm" */, ); preferredProjectObjectVersion = 77; productRefGroup = 66B6907B2E3FCDF20013519B /* Products */; @@ -155,6 +162,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6602CB632E6AF792005A4D38 /* EntityInfo-TakeNote.generated.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -429,6 +437,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 6602CB552E683E54005A4D38 /* XCRemoteSwiftPackageReference "objectbox-swift-spm" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/objectbox/objectbox-swift-spm"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.4.1; + }; + }; 66AC246D2E4609FB009B0C3F /* XCRemoteSwiftPackageReference "CodeEditorView" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/mchakravarty/CodeEditorView"; @@ -456,6 +472,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 6602CB562E683E54005A4D38 /* ObjectBox.xcframework */ = { + isa = XCSwiftPackageProductDependency; + package = 6602CB552E683E54005A4D38 /* XCRemoteSwiftPackageReference "objectbox-swift-spm" */; + productName = ObjectBox.xcframework; + }; 66AC246E2E4609FB009B0C3F /* CodeEditorView */ = { isa = XCSwiftPackageProductDependency; package = 66AC246D2E4609FB009B0C3F /* XCRemoteSwiftPackageReference "CodeEditorView" */; diff --git a/TakeNote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TakeNote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 697f066..8d17531 100644 --- a/TakeNote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TakeNote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "503a7a4ed322a8c6da355ccfb362b57a9b93135941bba5f38f832af33e242ab8", + "originHash" : "ac3e86675a456bb0bb159a657fdfb5bdf416e29b8b4db9f82d2e98c07d565126", "pins" : [ { "identity" : "codeeditorview", @@ -19,6 +19,15 @@ "version" : "6.0.1" } }, + { + "identity" : "objectbox-swift-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/objectbox/objectbox-swift-spm", + "state" : { + "revision" : "28c3261c9836cd3f4d64ab6419a3628d2b167811", + "version" : "4.4.1" + } + }, { "identity" : "rearrange", "kind" : "remoteSourceControl", diff --git a/TakeNote/Info.plist b/TakeNote/Info.plist index baec4c2..d25b294 100644 --- a/TakeNote/Info.plist +++ b/TakeNote/Info.plist @@ -2,46 +2,41 @@ - - - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - com.adamdrew.takenote - CFBundleURLSchemes - - takenote - - - - - UIBackgroundModes - - remote-notification - - - - UILaunchStoryboardName - Launch Screen - - UTExportedTypeDeclarations - - - UTTypeIdentifier - com.adamdrew.takenote.noteid - UTTypeDescription - Note ID - UTTypeConformsTo - - public.data - - - UTTypeTagSpecification - - - + MagicChatEnabled + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.adamdrew.takenote + CFBundleURLSchemes + + takenote + + + + UIBackgroundModes + + remote-notification + + UILaunchStoryboardName + Launch Screen + UTExportedTypeDeclarations + + + UTTypeIdentifier + com.adamdrew.takenote.noteid + UTTypeDescription + Note ID + UTTypeConformsTo + + public.data + + UTTypeTagSpecification + + + - \ No newline at end of file + diff --git a/TakeNote/Library/EmbeddingProvider.swift b/TakeNote/Library/EmbeddingProvider.swift new file mode 100644 index 0000000..509d8d3 --- /dev/null +++ b/TakeNote/Library/EmbeddingProvider.swift @@ -0,0 +1,35 @@ +// +// EmbeddingProvider.swift +// TakeNote +// +// Created by Adam Drew on 9/5/25. +// + +import NaturalLanguage + +class EmbeddingProvider { + private let model: NLEmbedding? + private let dim: Int? + + init(language: NLLanguage = .english, revision: Int? = nil) { + if let rev = revision { + self.model = NLEmbedding.sentenceEmbedding(for: language, revision: rev) + } else { + self.model = NLEmbedding.sentenceEmbedding(for: language) + } + self.dim = model?.dimension + } + + /// Returns a unit-length Float vector or nil if unavailable + func embed(_ text: String) -> [Float]? { + guard let model else { return nil } + guard let v = model.vector(for: text) else { return nil } // [Double] + var f = v.map { Float($0) } + // L2 normalize + let norm = sqrt(max(1e-12, f.reduce(0) { $0 + $1*$1 })) + for i in 0.. Store? { + let logger = Logger(subsystem: "com.adamdrew.takenote", category: "OBStoreMaker") + + do { + // 1) Build a sane on-disk path + let appSupport = try FileManager.default.url(for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true) + let bundleID = Bundle.main.bundleIdentifier ?? "com.adamdrew.takenote" + let directory = appSupport + .appendingPathComponent(bundleID, isDirectory: true) + .appendingPathComponent("chunks", isDirectory: true) + + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + + logger.info("OB dir: \(directory.path, privacy: .public) exists: \(FileManager.default.fileExists(atPath: directory.path)) writable: \(FileManager.default.isWritableFile(atPath: directory.path))") + + // 2) Try on-disk store + let store = try Store(directoryPath: directory.path) + logger.info("ObjectBox store opened") + return store + } catch { + logger.error("ObjectBox on-disk open failed: \(String(describing: error), privacy: .public)") + } + + // 3) Safe dev fallback: in-memory (keeps app from crashing) + do { + let mem = try Store(directoryPath: "memory:chunks-dev") + Logger(subsystem: "com.adamdrew.takenote", category: "OBStoreMaker") + .info("Using in-memory ObjectBox store (dev fallback)") + return mem + } catch { + Logger(subsystem: "com.adamdrew.takenote", category: "OBStoreMaker") + .error("ObjectBox in-memory open failed too: \(String(describing: error), privacy: .public)") + return nil + } +} + +class OBSearchIndex { + let logger = Logger( + subsystem: "com.adamdrew.takenote", + category: "OBSearchIndex" + ) + private let chunker: WindowChunker + private let embedder: EmbeddingProvider + private let store : Store? + private let box : Box + + + init(inMemory: Bool = true) { + self.chunker = WindowChunker() + self.embedder = EmbeddingProvider() + self.store = makeStore() + guard let s = store else { + logger.error("OBSearchIndex: store unavailable; running with no search backend") + fatalError("OB store failed") + } + self.box = s.box(for: OBChunk.self) + + } + + // MARK: Public API + + /// Index 1 note (replace all its chunks) + func reindex(noteID: UUID, markdown: String) { + delete(noteID: noteID) + for chunk in chunker.chunks(for: markdown) { + let embedding = embedder.embed(chunk.text) + let obChunk = OBChunk(noteID: noteID, chunk: chunk.text, embedding: embedding) + do { + try self.box.put(obChunk) + } catch { + logger.error("Error persisting chunk: \(error)") + } + } + logger.debug("Indexed note \(noteID)") + } + + /// Index many notes (replace each note's chunks) + func reindex(_ notes: [(UUID, String)]) { + for note in notes { + reindex(noteID: note.0, markdown: note.1) + } + } + + /// Destroy all records + func dropAll() { + do { + try self.box.removeAll() + logger.info("Deleted all chunks from the objectbox store") + } catch { + self.logger.error("Error deleting everything from store box: \(error)") + } + } + + /// Remove a single note’s chunks + func delete(noteID: UUID) { + do { + let query: Query = try self.box.query { + OBChunk.noteID == noteID.uuidString + }.build() + let chunks: [OBChunk] = try query.find() + for chunk in chunks { + try self.box.remove(chunk.id) + } + self.logger.info("Removed chunk from store") + } catch { + self.logger.error("Error deleting chunks: \(error)") + } + } + + /// Back-compat alias + func searchNatural(_ text: String, limit: Int = 5) -> [SearchHit] { + return search(text, limit: limit) + } + + /// Dense (cosine) search over embedded chunks with light anchor scoping + func search(_ query: String, limit: Int = 5) -> [SearchHit] { + guard let embedding = embedder.embed(query) else { + logger.error("Error generating query embedding") + return [] + } + do { + let query = try box + .query { OBChunk.embedding.nearestNeighbors(queryVector: embedding, maxCount: 4) } + .build() + let results = try query.findWithScores() + let searchHits: [SearchHit] = results.map { result in + let id = result.object.id + let uuid = UUID(uuidString: result.object.noteID) + let chunk = result.object.chunk + return SearchHit(id: Int64(id), noteID: uuid ?? UUID(), chunk: chunk) + } + logger.info("OBS: Found \(results.count) search results") + return searchHits + } catch { + logger.error("Error performing vector search: \(error)") + } + return [] + } + + +} diff --git a/TakeNote/Library/SearchHit.swift b/TakeNote/Library/SearchHit.swift new file mode 100644 index 0000000..23e8e7f --- /dev/null +++ b/TakeNote/Library/SearchHit.swift @@ -0,0 +1,14 @@ +// +// SearchHit.swift +// TakeNote +// +// Created by Adam Drew on 9/5/25. +// + +import Foundation + +struct SearchHit: Identifiable { + public let id: Int64 // rowid inside FTS table + public let noteID: UUID + public let chunk: String // the stored chunk text +} diff --git a/TakeNote/Library/SearchIndex.swift b/TakeNote/Library/SearchIndex.swift index a6f1b2f..d810b02 100644 --- a/TakeNote/Library/SearchIndex.swift +++ b/TakeNote/Library/SearchIndex.swift @@ -31,11 +31,6 @@ internal final class SearchIndex { "t", "can", "will", "just", "don", "should", "now"] // MARK: Result type - struct SearchHit: Identifiable { - public let id: Int64 // rowid inside FTS table - public let noteID: UUID - public let chunk: String // the stored chunk text - } // MARK: Schema (DSL handles) private let fts = VirtualTable("fts") diff --git a/TakeNote/Library/SearchIndexService.swift b/TakeNote/Library/SearchIndexService.swift index 3c7270d..8832659 100644 --- a/TakeNote/Library/SearchIndexService.swift +++ b/TakeNote/Library/SearchIndexService.swift @@ -12,26 +12,33 @@ import os @Observable class SearchIndexService { #if DEBUG - let index = try! SearchIndex(inMemory: true) + let index = VectorSearchIndex(inMemory: true) #else - let index = try! SearchIndex() + let index = VectorSearchIndex() #endif - var hits: [SearchIndex.SearchHit] = [] + // Explicitly use the top-level SearchHit type to avoid the macro qualifying it as SearchIndex.SearchHit + var hits: [SearchHit] = [] var isIndexing: Bool = false var lastReindexAllDate: Date = .distantPast - var logger = Logger(subsystem: "com.adammdrew.takenote", category: "SearchIndexService") + var logger = Logger(subsystem: "com.adamdrew.takenote", category: "SearchIndexService") + var chatFeatureFlagEnabled : Bool { + return Bundle.main.object(forInfoDictionaryKey: "MagicChatenabled") as? Bool ?? false + } + func canReindexAllNotes() -> Bool { if isIndexing { return false } return Date().timeIntervalSince(lastReindexAllDate) >= 10 * 60 } func reindex(note: Note) { + if !chatFeatureFlagEnabled { return } Task { index.reindex(noteID: note.uuid, markdown: note.content) } } func reindexAll(_ noteData: [(UUID, String)]) { + if !chatFeatureFlagEnabled { return } if !canReindexAllNotes() { return } logger.info("RAG search reindex running.") lastReindexAllDate = Date() @@ -43,6 +50,7 @@ class SearchIndexService { } func dropAll() { + if !chatFeatureFlagEnabled { return } Task { index.dropAll() } } diff --git a/TakeNote/Library/VectorSearchIndex.swift b/TakeNote/Library/VectorSearchIndex.swift new file mode 100644 index 0000000..5d20d39 --- /dev/null +++ b/TakeNote/Library/VectorSearchIndex.swift @@ -0,0 +1,148 @@ +// +// VectorSearchIndex.swift +// TakeNote +// +// Created by Adam Drew on 9/3/25. +// + +import Foundation +import NaturalLanguage +import os + +// MARK: - Public API-compatible adapter + +final class VectorSearchIndex { + let logger = Logger(subsystem: "com.adamdrew.takenote", category: "VectorSearchIndex") + private let chunker: WindowChunker + private let embedder: EmbeddingProvider + + // In-memory store of embedded chunks (swap to SwiftData/ObjectBox later) + private var chunks: [ChunkRecord] = [] + private var nextRowID: Int64 = 1 + + // Internal chunk record + private struct ChunkRecord { + let id: Int64 + let noteID: UUID + let text: String + let vector: [Float] // L2-normalized + let updatedAt: Date + } + + // MARK: Init + init(inMemory: Bool = true) { + self.chunker = WindowChunker() + self.embedder = EmbeddingProvider() + } + + // MARK: Indexing + + /// Index 1 note (replace all its chunks) + func reindex(noteID: UUID, markdown: String) { + delete(noteID: noteID) + let newChunks = makeChunks(noteID: noteID, markdown: markdown) + chunks.append(contentsOf: newChunks) + logger.debug("Reindexed note \(noteID, privacy: .public) with \(newChunks.count) chunk(s)") + } + + /// Index many notes (replace each note's chunks) + func reindex(_ notes: [(UUID, String)]) { + let ids = Set(notes.map { $0.0 }) + chunks.removeAll { ids.contains($0.noteID) } + var added = 0 + for (id, md) in notes { + let cs = makeChunks(noteID: id, markdown: md) + chunks.append(contentsOf: cs) + added += cs.count + } + logger.debug("Bulk reindex completed. Notes: \(notes.count), Chunks added: \(added)") + } + + /// Destroy entire in-memory index + func dropAll() { + chunks.removeAll() + nextRowID = 1 + logger.debug("VectorSearchIndex dropAll(): cleared in-memory index") + } + + /// Remove a single note’s chunks + func delete(noteID: UUID) { + let before = chunks.count + chunks.removeAll { $0.noteID == noteID } + let removed = before - chunks.count + if removed > 0 { + logger.debug("Deleted \(removed) chunk(s) for note \(noteID, privacy: .public)") + } + } + + // MARK: Search + + /// Back-compat alias + func searchNatural(_ text: String, limit: Int = 5) -> [SearchHit] { + return search(text, limit: limit) + } + + /// Dense (cosine) search over embedded chunks with light anchor scoping + func search(_ query: String, limit: Int = 5) -> [SearchHit] { + guard !chunks.isEmpty else { return [] } + + let candidates = chunks + + // 2) Embed the query once (sentence embedding) + guard let qvec = embedder.embed(query) else { return [] } + + // 3) Cosine = dot product (vectors are unit-normalized) + // Use a tiny fixed-size min-heap pattern to avoid sorting all scores + let k = max(limit, 1) + var heap: [(score: Float, idx: Int)] = [] // min-heap (score asc) + + func push(_ item: (Float, Int)) { + heap.append(item) + heap.sort { $0.score < $1.score } // small k: this is fine & simple + if heap.count > k { _ = heap.removeFirst() } + } + + for (i, c) in candidates.enumerated() { + let s = dot(qvec, c.vector) + push((s, i)) + } + + // 4) Highest scores last in heap; return in descending score order + let top = heap.sorted { $0.score > $1.score } + return top.map { hit in + let rec = candidates[hit.idx] + return SearchHit(id: rec.id, noteID: rec.noteID, chunk: rec.text) + } + } + + // MARK: - Helpers + + private func makeChunks(noteID: UUID, markdown: String) -> [ChunkRecord] { + // NOTE: your current WindowChunker has no overlap; keep as-is for now. + // If you add overlap later, this method doesn’t change. + let parts = chunker.chunks(for: markdown) + var out: [ChunkRecord] = [] + out.reserveCapacity(parts.count) + + for p in parts { + guard let vec = embedder.embed(p.text) else { continue } + out.append(.init( + id: nextRowID, + noteID: noteID, + text: p.text, + vector: vec, + updatedAt: Date() + )) + nextRowID &+= 1 + } + return out + } + + @inline(__always) + private func dot(_ a: [Float], _ b: [Float]) -> Float { + // Preconditions should hold: equal length, both unit vectors + var s: Float = 0 + for i in 0.. { + return EntityId(self.id.value) + } +} + +extension OBChunk: ObjectBox.EntityInspectable { + internal typealias EntityBindingType = OBChunkBinding + + /// Generated metadata used by ObjectBox to persist the entity. + internal static let entityInfo = ObjectBox.EntityInfo(name: "OBChunk", id: 1) + + internal static let entityBinding = EntityBindingType() + + fileprivate static func buildEntity(modelBuilder: ObjectBox.ModelBuilder) throws { + let entityBuilder = try modelBuilder.entityBuilder(for: OBChunk.self, id: 1, uid: 8777672549323939584) + try entityBuilder.addProperty(name: "id", type: PropertyType.long, flags: [.id], id: 1, uid: 8772012977392055552) + try entityBuilder.addProperty(name: "noteID", type: PropertyType.string, id: 2, uid: 1538130122119906560) + try entityBuilder.addProperty(name: "chunk", type: PropertyType.string, id: 3, uid: 1568624585990606336) + try entityBuilder.addProperty(name: "embedding", type: PropertyType.floatVector, flags: [.indexed], id: 4, uid: 7385883355930001408, indexId: 1, indexUid: 8749910346665274624) + .hnswParams(dimensions: 512, neighborsPerNode: nil, indexingSearchCount: nil, flags: nil, distanceType: HnswDistanceType.cosine, reparationBacklinkProbability: nil, vectorCacheHintSizeKB: nil) + + try entityBuilder.lastProperty(id: 4, uid: 7385883355930001408) + } +} + +extension OBChunk { + /// Generated entity property information. + /// + /// You may want to use this in queries to specify fetch conditions, for example: + /// + /// box.query { OBChunk.id == myId } + internal static var id: Property { return Property(propertyId: 1, isPrimaryKey: true) } + /// Generated entity property information. + /// + /// You may want to use this in queries to specify fetch conditions, for example: + /// + /// box.query { OBChunk.noteID.startsWith("X") } + internal static var noteID: Property { return Property(propertyId: 2, isPrimaryKey: false) } + /// Generated entity property information. + /// + /// You may want to use this in queries to specify fetch conditions, for example: + /// + /// box.query { OBChunk.chunk.startsWith("X") } + internal static var chunk: Property { return Property(propertyId: 3, isPrimaryKey: false) } + /// Generated entity property information. + /// + /// You may want to use this in queries to specify fetch conditions, for example: + /// + /// box.query { OBChunk.embedding.isNotNil() } + internal static var embedding: Property { return Property(propertyId: 4, isPrimaryKey: false) } + + fileprivate func __setId(identifier: ObjectBox.Id) { + self.id = Id(identifier) + } +} + +extension ObjectBox.Property where E == OBChunk { + /// Generated entity property information. + /// + /// You may want to use this in queries to specify fetch conditions, for example: + /// + /// box.query { .id == myId } + + internal static var id: Property { return Property(propertyId: 1, isPrimaryKey: true) } + + /// Generated entity property information. + /// + /// You may want to use this in queries to specify fetch conditions, for example: + /// + /// box.query { .noteID.startsWith("X") } + + internal static var noteID: Property { return Property(propertyId: 2, isPrimaryKey: false) } + + /// Generated entity property information. + /// + /// You may want to use this in queries to specify fetch conditions, for example: + /// + /// box.query { .chunk.startsWith("X") } + + internal static var chunk: Property { return Property(propertyId: 3, isPrimaryKey: false) } + + /// Generated entity property information. + /// + /// You may want to use this in queries to specify fetch conditions, for example: + /// + /// box.query { .embedding.isNotNil() } + + internal static var embedding: Property { return Property(propertyId: 4, isPrimaryKey: false) } + +} + + +/// Generated service type to handle persisting and reading entity data. Exposed through `OBChunk.EntityBindingType`. +internal final class OBChunkBinding: ObjectBox.EntityBinding, Sendable { + internal typealias EntityType = OBChunk + internal typealias IdType = Id + + internal required init() {} + + internal func generatorBindingVersion() -> Int { 1 } + + internal func setEntityIdUnlessStruct(of entity: EntityType, to entityId: ObjectBox.Id) { + entity.__setId(identifier: entityId) + } + + internal func entityId(of entity: EntityType) -> ObjectBox.Id { + return entity.id.value + } + + internal func collect(fromEntity entity: EntityType, id: ObjectBox.Id, + propertyCollector: ObjectBox.FlatBufferBuilder, store: ObjectBox.Store) throws { + let propertyOffset_noteID = propertyCollector.prepare(string: entity.noteID) + let propertyOffset_chunk = propertyCollector.prepare(string: entity.chunk) + let propertyOffset_embedding = propertyCollector.prepare(values: entity.embedding) + + propertyCollector.collect(id, at: 2 + 2 * 1) + propertyCollector.collect(dataOffset: propertyOffset_noteID, at: 2 + 2 * 2) + propertyCollector.collect(dataOffset: propertyOffset_chunk, at: 2 + 2 * 3) + propertyCollector.collect(dataOffset: propertyOffset_embedding, at: 2 + 2 * 4) + } + + internal func createEntity(entityReader: ObjectBox.FlatBufferReader, store: ObjectBox.Store) -> EntityType { + let entity = OBChunk() + + entity.id = entityReader.read(at: 2 + 2 * 1) + entity.noteID = entityReader.read(at: 2 + 2 * 2) + entity.chunk = entityReader.read(at: 2 + 2 * 3) + entity.embedding = entityReader.read(at: 2 + 2 * 4) + + return entity + } +} + + +/// Helper function that allows calling Enum(rawValue: value) with a nil value, which will return nil. +fileprivate func optConstruct(_ type: T.Type, rawValue: T.RawValue?) -> T? { + guard let rawValue = rawValue else { return nil } + return T(rawValue: rawValue) +} + +// MARK: - Store setup + +fileprivate func cModel() throws -> OpaquePointer { + let modelBuilder = try ObjectBox.ModelBuilder() + try OBChunk.buildEntity(modelBuilder: modelBuilder) + modelBuilder.lastEntity(id: 1, uid: 8777672549323939584) + modelBuilder.lastIndex(id: 1, uid: 8749910346665274624) + return modelBuilder.finish() +} + +extension ObjectBox.Store { + /// A store with a fully configured model. Created by the code generator with your model's metadata in place. + /// + /// # In-memory database + /// To use a file-less in-memory database, instead of a directory path pass `memory:` + /// together with an identifier string: + /// ```swift + /// let inMemoryStore = try Store(directoryPath: "memory:test-db") + /// ``` + /// + /// - Parameters: + /// - directoryPath: The directory path in which ObjectBox places its database files for this store, + /// or to use an in-memory database `memory:`. + /// - maxDbSizeInKByte: Limit of on-disk space for the database files. Default is `1024 * 1024` (1 GiB). + /// - fileMode: UNIX-style bit mask used for the database files; default is `0o644`. + /// Note: directories become searchable if the "read" or "write" permission is set (e.g. 0640 becomes 0750). + /// - maxReaders: The maximum number of readers. + /// "Readers" are a finite resource for which we need to define a maximum number upfront. + /// The default value is enough for most apps and usually you can ignore it completely. + /// However, if you get the maxReadersExceeded error, you should verify your + /// threading. For each thread, ObjectBox uses multiple readers. Their number (per thread) depends + /// on number of types, relations, and usage patterns. Thus, if you are working with many threads + /// (e.g. in a server-like scenario), it can make sense to increase the maximum number of readers. + /// Note: The internal default is currently around 120. So when hitting this limit, try values around 200-500. + /// - readOnly: Opens the database in read-only mode, i.e. not allowing write transactions. + /// + /// - important: This initializer is created by the code generator. If you only see the internal `init(model:...)` + /// initializer, trigger code generation by building your project. + internal convenience init(directoryPath: String, maxDbSizeInKByte: UInt64 = 1024 * 1024, + fileMode: UInt32 = 0o644, maxReaders: UInt32 = 0, readOnly: Bool = false) throws { + try self.init( + model: try cModel(), + directory: directoryPath, + maxDbSizeInKByte: maxDbSizeInKByte, + fileMode: fileMode, + maxReaders: maxReaders, + readOnly: readOnly) + } +} + +// swiftlint:enable all diff --git a/model-TakeNote.json b/model-TakeNote.json new file mode 100644 index 0000000..1939db6 --- /dev/null +++ b/model-TakeNote.json @@ -0,0 +1,49 @@ +{ + "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", + "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", + "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", + "entities": [ + { + "id": "1:8777672549323939584", + "lastPropertyId": "4:7385883355930001408", + "name": "OBChunk", + "properties": [ + { + "id": "1:8772012977392055552", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:1538130122119906560", + "name": "noteID", + "type": 9 + }, + { + "id": "3:1568624585990606336", + "name": "chunk", + "type": 9 + }, + { + "id": "4:7385883355930001408", + "name": "embedding", + "indexId": "1:8749910346665274624", + "type": 28, + "flags": 8 + } + ], + "relations": [] + } + ], + "lastEntityId": "1:8777672549323939584", + "lastIndexId": "1:8749910346665274624", + "lastRelationId": "0:0", + "lastSequenceId": "0:0", + "modelVersion": 5, + "modelVersionParserMinimum": 4, + "retiredEntityUids": [], + "retiredIndexUids": [], + "retiredPropertyUids": [], + "retiredRelationUids": [], + "version": 1 +} \ No newline at end of file diff --git a/model-TakeNote.json.bak b/model-TakeNote.json.bak new file mode 100644 index 0000000..54ed5ea --- /dev/null +++ b/model-TakeNote.json.bak @@ -0,0 +1,48 @@ +{ + "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", + "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", + "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", + "entities": [ + { + "id": "1:8777672549323939584", + "lastPropertyId": "4:7385883355930001408", + "name": "OBChunk", + "properties": [ + { + "id": "1:8772012977392055552", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:1538130122119906560", + "name": "noteID" + }, + { + "id": "3:1568624585990606336", + "name": "chunk", + "type": 9 + }, + { + "id": "4:7385883355930001408", + "name": "embedding", + "indexId": "1:8749910346665274624", + "type": 28, + "flags": 8 + } + ], + "relations": [] + } + ], + "lastEntityId": "1:8777672549323939584", + "lastIndexId": "1:8749910346665274624", + "lastRelationId": "0:0", + "lastSequenceId": "0:0", + "modelVersion": 5, + "modelVersionParserMinimum": 4, + "retiredEntityUids": [], + "retiredIndexUids": [], + "retiredPropertyUids": [], + "retiredRelationUids": [], + "version": 1 +} \ No newline at end of file