From 189394d3373700dc53f004c991f34a87853607cb Mon Sep 17 00:00:00 2001 From: Tom Ludwig <83090745+activcoding@users.noreply.github.com> Date: Mon, 6 Nov 2023 20:40:19 +0100 Subject: [PATCH] Added the async manager --- SearchKitDemo.xcodeproj/project.pbxproj | 8 + .../Indexer/SearchIndexer+AsyncManager.swift | 172 ++++++++++++++++++ .../SearchIndexer+ProgressivSearch.swift | 12 +- .../Indexer/SearchIndexer+Terms.swift | 7 +- SearchKitDemo/Indexer/SearchIndexer.swift | 5 +- .../AsyncIndexSearchingTests.swift | 37 ++++ 6 files changed, 223 insertions(+), 18 deletions(-) create mode 100644 SearchKitDemo/Indexer/SearchIndexer+AsyncManager.swift create mode 100644 SearchKitDemoTests/AsyncIndexSearchingTests.swift diff --git a/SearchKitDemo.xcodeproj/project.pbxproj b/SearchKitDemo.xcodeproj/project.pbxproj index 1de0fa1..2b34ea2 100644 --- a/SearchKitDemo.xcodeproj/project.pbxproj +++ b/SearchKitDemo.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 617658A32AF161AD000C5197 /* SearchKitDemoUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617658A22AF161AD000C5197 /* SearchKitDemoUITests.swift */; }; 617658A52AF161AD000C5197 /* SearchKitDemoUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617658A42AF161AD000C5197 /* SearchKitDemoUITestsLaunchTests.swift */; }; 617658B42AF16B04000C5197 /* SearchManger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617658B32AF16B04000C5197 /* SearchManger.swift */; }; + 617D04BF2AF9769300CD2CDA /* AsyncIndexSearchingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617D04BE2AF9769300CD2CDA /* AsyncIndexSearchingTests.swift */; }; + 619DD8012AF962A000A9364E /* SearchIndexer+AsyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619DD8002AF962A000A9364E /* SearchIndexer+AsyncManager.swift */; }; 61E2E7802AF3EDDC00E3AA4A /* SearchIndexer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E2E77F2AF3EDDC00E3AA4A /* SearchIndexer.swift */; }; 61E2E7822AF3F7F800E3AA4A /* SearchIndexer+Add.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E2E7812AF3F7F800E3AA4A /* SearchIndexer+Add.swift */; }; /* End PBXBuildFile section */ @@ -74,6 +76,8 @@ 617658A22AF161AD000C5197 /* SearchKitDemoUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchKitDemoUITests.swift; sourceTree = ""; }; 617658A42AF161AD000C5197 /* SearchKitDemoUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchKitDemoUITestsLaunchTests.swift; sourceTree = ""; }; 617658B32AF16B04000C5197 /* SearchManger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchManger.swift; sourceTree = ""; }; + 617D04BE2AF9769300CD2CDA /* AsyncIndexSearchingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncIndexSearchingTests.swift; sourceTree = ""; }; + 619DD8002AF962A000A9364E /* SearchIndexer+AsyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchIndexer+AsyncManager.swift"; sourceTree = ""; }; 61E2E77F2AF3EDDC00E3AA4A /* SearchIndexer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchIndexer.swift; sourceTree = ""; }; 61E2E7812AF3F7F800E3AA4A /* SearchIndexer+Add.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchIndexer+Add.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -154,6 +158,7 @@ 617658982AF161AD000C5197 /* SearchKitDemoTests.swift */, 6129F87F2AF555E800A10F67 /* InMemoryIndexing.swift */, 6129F8862AF6BD5900A10F67 /* FileIndexingTests.swift */, + 617D04BE2AF9769300CD2CDA /* AsyncIndexSearchingTests.swift */, 6129F8882AF6BDD700A10F67 /* FileHelper.swift */, ); path = SearchKitDemoTests; @@ -180,6 +185,7 @@ isa = PBXGroup; children = ( 61E2E77F2AF3EDDC00E3AA4A /* SearchIndexer.swift */, + 619DD8002AF962A000A9364E /* SearchIndexer+AsyncManager.swift */, 6129F8842AF6BAAF00A10F67 /* SearchIndexer+File.swift */, 6129F8822AF556B000A10F67 /* SearchIndexer+Memory.swift */, 6129F8772AF50CE600A10F67 /* SearchIndexer+Search.swift */, @@ -332,6 +338,7 @@ 6129F87C2AF5522E00A10F67 /* SearchIndexer+Indexer.swift in Sources */, 6129F87A2AF5211B00A10F67 /* SearchIndexer+ProgressivSearch.swift in Sources */, 617658842AF161AB000C5197 /* SearchKitDemoApp.swift in Sources */, + 619DD8012AF962A000A9364E /* SearchIndexer+AsyncManager.swift in Sources */, 617658882AF161AB000C5197 /* ContentView.swift in Sources */, 6129F8782AF50CE600A10F67 /* SearchIndexer+Search.swift in Sources */, 6129F8762AF502DE00A10F67 /* SearchIndexer+Terms.swift in Sources */, @@ -347,6 +354,7 @@ 6129F8802AF555E800A10F67 /* InMemoryIndexing.swift in Sources */, 617658992AF161AD000C5197 /* SearchKitDemoTests.swift in Sources */, 6129F8892AF6BDD700A10F67 /* FileHelper.swift in Sources */, + 617D04BF2AF9769300CD2CDA /* AsyncIndexSearchingTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SearchKitDemo/Indexer/SearchIndexer+AsyncManager.swift b/SearchKitDemo/Indexer/SearchIndexer+AsyncManager.swift new file mode 100644 index 0000000..1651c14 --- /dev/null +++ b/SearchKitDemo/Indexer/SearchIndexer+AsyncManager.swift @@ -0,0 +1,172 @@ +// +// SearchIndexer+AsyncManager.swift +// SearchKitDemo +// +// Created by Tommy Ludwig on 06.11.23. +// + +import Foundation + +extension SearchIndexer { + /// Manager for SearchIndexer objct that supports async calls to the index + public class AsyncManager { + let index: SearchIndexer + + + /// Queue for handling async modifications to the index + // fileprivate let modifyQueue = DispatchQueue(label: "com.SearchkitDemo.modifyQueue", attributes: .concurrent) + + init(index: SearchIndexer) { + self.index = index + } + + + class TextTask { + let url: URL + let text: String + + /// Create a text async task + /// + /// - Parameters: + /// - url: the identifying document URL + /// - text: The text to add to the index + init(url: URL, text: String) { + self.url = url + self.text = text + } + } + + /// A task nor handling searches + class SearchTask: AsyncManager { + private var search: SearchIndexer.ProgressivSearch + + let query: String + + private let searchQueue = DispatchQueue(label: "com.SearchkitDemo.searchQueue", attributes: .concurrent) + + init(_ index: SearchIndexer, query: String) { + self.query = query + self.search = index.progressiveSearch(query: query) + super.init(index: index) + } + + deinit { + self.search.cancel() + } + + func next( + _ maxResults: Int, + timeout: TimeInterval = 1.0, + complete: @escaping (SearchTask, SearchIndexer.ProgressivSearch.Results) -> Void + ) { + searchQueue.async { + let results = self.search.next(maxResults, timeout: timeout) + let searchResults = SearchIndexer.ProgressivSearch.Results(moreResultsAvailable: results.moreResultsAvailable, results: results.results) + + DispatchQueue.main.async { + complete(self, searchResults) + } + } + } + } + + class AddTask: AsyncManager { + private let addQueue = DispatchQueue(label: "com.SearchkitDemo.addQueue", attributes: .concurrent) + + func addText( + async textTask: [TextTask], + flushWhenComplete: Bool = false, + complete: @escaping ([TextTask]) -> Void + ) { + let dispatchGroup = DispatchGroup() + + for task in textTask { + dispatchGroup.enter() + addQueue.async { [weak self] in + guard let self = self else { return } + let _ = self.index.add(task.url, text: task.text) + dispatchGroup.leave() + } + } + + dispatchGroup.notify(queue: .main) { + if flushWhenComplete { + self.index.flush() + } + complete(textTask) + } + } + + func addFiles( + urls: [URL], + flushWhenComplete: Bool = false + ) { + let dispatchGroup = DispatchGroup() + + for url in urls { + dispatchGroup.enter() + addQueue.async { [weak self] in + guard let self = self else { return } + let _ = self.index.add(fileURL: url, canReplace: false) + dispatchGroup.leave() + } + } + + dispatchGroup.notify(queue: .main) { + if flushWhenComplete { + self.index.flush() + } + } + } + + func addFolder( + url: URL, + flushWhenComplete: Bool = false + ) { + let dispatchGroup = DispatchGroup() + + let fileManager = FileManager.default + let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles], errorHandler: nil)! + + for case let fileURL as URL in enumerator { + dispatchGroup.enter() + + if FileHelper.urlIsFolder(url) { + addQueue.async { [weak self] in + guard let self = self else { return } + self.addFolder(url: url) + dispatchGroup.leave() + } + } else { + addQueue.async { [weak self] in + guard let self = self else { return } + let _ = self.index.add(fileURL: fileURL, canReplace: false) + dispatchGroup.leave() + } + } + } + + dispatchGroup.notify(queue: .main) { + if flushWhenComplete { + self.index.flush() + } + } + } + } + } +} + +class FileHelper { + static func urlIsFolder(_ url: URL) -> Bool { + var isDirectory: ObjCBool = false + let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) + return exists && isDirectory.boolValue + } + + static func urlIsFile(_ url: URL) -> Bool { + var isDirectory: ObjCBool = false + let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) + return exists && !isDirectory.boolValue + } +} + diff --git a/SearchKitDemo/Indexer/SearchIndexer+ProgressivSearch.swift b/SearchKitDemo/Indexer/SearchIndexer+ProgressivSearch.swift index 037879c..602fc8a 100644 --- a/SearchKitDemo/Indexer/SearchIndexer+ProgressivSearch.swift +++ b/SearchKitDemo/Indexer/SearchIndexer+ProgressivSearch.swift @@ -9,7 +9,7 @@ import Foundation extension SearchIndexer { /// Object representaitng the search results - public class SearchResult: NSObject { + public class SearchResult { /// The identifying url for the document public let url: URL @@ -19,11 +19,6 @@ extension SearchIndexer { init(url: URL, score: Float) { self.url = url self.score = score - super.init() - } - - public override var debugDescription: String { - return "Score: \(self.score), URL: \(self.url)" } } @@ -35,8 +30,8 @@ extension SearchIndexer { return ProgressivSearch(options: options, index: self, query: query) } - public class ProgressivSearch: NSObject { - public class Results: NSObject { + public class ProgressivSearch { + public class Results { /// Create a search result /// /// - Parameters: @@ -45,7 +40,6 @@ extension SearchIndexer { public init(moreResultsAvailable: Bool, results: [SearchResult]) { self.moreResultsAvailable = moreResultsAvailable self.results = results - super.init() } /// A boolean indicating whether more search results are available diff --git a/SearchKitDemo/Indexer/SearchIndexer+Terms.swift b/SearchKitDemo/Indexer/SearchIndexer+Terms.swift index 300e67e..875b76c 100644 --- a/SearchKitDemo/Indexer/SearchIndexer+Terms.swift +++ b/SearchKitDemo/Indexer/SearchIndexer+Terms.swift @@ -9,7 +9,7 @@ import Foundation extension SearchIndexer { /// A class to contain a term and the count of times it appears - public class TermCount: NSObject { + public class TermCount { /// A term within the document public let term: String @@ -19,11 +19,6 @@ extension SearchIndexer { init(term: String, count: Int) { self.term = term self.count = count - super.init() - } - - public override var debugDescription: String { - return "Term: '\(self.term)', Count: \(self.count)'" } } diff --git a/SearchKitDemo/Indexer/SearchIndexer.swift b/SearchKitDemo/Indexer/SearchIndexer.swift index d910385..1b0a196 100644 --- a/SearchKitDemo/Indexer/SearchIndexer.swift +++ b/SearchKitDemo/Indexer/SearchIndexer.swift @@ -28,7 +28,7 @@ public class Synchronised { } /// Indexer using SKIndex -public class SearchIndexer: NSObject { +public class SearchIndexer { let queue = DispatchQueue(label: "com.activcoding.SearchKitDemo") public enum IndexType: UInt32 { @@ -42,7 +42,7 @@ public class SearchIndexer: NSObject { case invertedVector = 3 } - public class CreateProperties: NSObject { + public class CreateProperties { /// The type of the index to be created private(set) var indexType: SKIndexType = kSKIndexInverted /// Whether the index should use proximity indexing @@ -112,7 +112,6 @@ public class SearchIndexer: NSObject { init(index: SKIndex) { self.index = index - super.init() } deinit { diff --git a/SearchKitDemoTests/AsyncIndexSearchingTests.swift b/SearchKitDemoTests/AsyncIndexSearchingTests.swift new file mode 100644 index 0000000..c515274 --- /dev/null +++ b/SearchKitDemoTests/AsyncIndexSearchingTests.swift @@ -0,0 +1,37 @@ +// +// AsyncIndexSearchingTests.swift +// SearchKitDemoTests +// +// Created by Tommy Ludwig on 06.11.23. +// + +@testable import SearchKitDemo +import XCTest + +final class AsyncIndexSearchingTests: XCTestCase { + fileprivate func bundleResourceURL(forResource name: String, withExtension ext: String) -> URL { + let thisSourceFile = URL(fileURLWithPath: #file) + var thisDirectory = thisSourceFile.deletingLastPathComponent() + thisDirectory = thisDirectory.appendingPathComponent("Resources") + thisDirectory = thisDirectory.appendingPathComponent(name + "." + ext) + return thisDirectory + } + fileprivate func bundleResourceFolderURL() -> URL { + let thisSourceFile = URL(fileURLWithPath: #file) + var thisDirectory = thisSourceFile.deletingLastPathComponent() + thisDirectory = thisDirectory.appendingPathComponent("Resources") + return thisDirectory + } + + func testAddDocuments() { + guard let indexer = SearchIndexer.Memory.Create() else { + XCTFail() + return + } + + let filePath = bundleResourceURL(forResource: "APACHE_LICENSE", withExtension: "pdf") + let txtPath = bundleResourceURL(forResource: "the_school_short_story", withExtension: "txt") + + let asyncManager = SearchIndexer.AsyncManager.SearchTask + } +}