├── .gitattributes ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── SwiftSemanticSearch.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── SwiftSemanticSearch.xcscheme ├── SwiftSemanticSearch ├── App.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── SwiftSemanticSearch.jpg │ └── Contents.json ├── ContentView.swift ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── SearchModel.swift ├── SwiftVectorSearch.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcuserdata │ └── av.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── SwiftVectorSearch ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── SwiftVectorSearchApp.swift ├── dataset.zip └── images.ipynb /.gitattributes: -------------------------------------------------------------------------------- 1 | dataset.zip filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /images 2 | /images.fbin 3 | /images.zip 4 | /images.csv 5 | /images.txt 6 | /images.names.txt 7 | /images.uform3-image-text-english-small.fbin 8 | /images.uform3-image-text-english-small.usearch 9 | 10 | /__MACOSX 11 | .DS_Store 12 | .swiftpm/ 13 | .swiftpm/* 14 | /.build 15 | /Packages 16 | xcuserdata/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Vardanian" 4 | ] 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Semantic Search 🍏 2 | 3 | ![Preview](https://github.com/ashvardanian/ashvardanian/blob/master/repositories/SwiftSemanticSearch.jpg?raw=true#center) 4 | 5 | This Swift demo app shows you how to build real-time native AI-powered apps for Apple devices using Unum's Swift libraries and quantized models. 6 | Under the hood, it uses [UForm](https://github.com/unum-cloud/uform) to understand and "embed" multimodal data, like multilingual texts and images, processing them on the fly from a camera feed. 7 | Once the vector embeddings are computed, it uses [USearch](https://github.com/unum-cloud/usearch) to provide a real-time search over the semantic space. 8 | That same engine also enables geo-spatial search over the coordinates of the images and has been shown to scale even to 100M+ entries on an 🍏 iPhone easily. 9 | 10 | 11 | 12 | 15 | 18 | 19 |
13 | SwiftSemanticSearch demo Dog 14 | 16 | SwiftSemanticSearch demo with Flowers 17 |
20 | 21 | The demo app is capable of text-to-image and image-to-image search and uses `vmanot/Media` libra to fetch the camera feed, embedding, and searching frames on the fly. 22 | To test the demo: 23 | 24 | ```bash 25 | # Clone the repo 26 | git clone https://github.com/ashvardanian/SwiftSemanticSearch.git 27 | 28 | # Change directory & decompress the images dataset.zip, which brings: 29 | # - `images.names.txt` with newline-separated image names 30 | # - `images.uform3-image-text-english-small.fbin` - precomputed embeddings 31 | # - `images.uform3-image-text-english-small.usearch` - precomputed index 32 | # - `images` - directory with images 33 | cd SwiftSemanticSearch 34 | unzip dataset.zip 35 | ``` 36 | 37 | After that, fire up the Xcode project and run the app on your fruity device! 38 | 39 | --- 40 | 41 | Links: 42 | 43 | - [Preprocessing datasets](https://github.com/ashvardanian/SwiftSemanticSearch/blob/main/images.ipynb) 44 | - [USearch Swift docs](https://unum-cloud.github.io/usearch/swift) 45 | - [Form Swift docs](https://unum-cloud.github.io/uform/swift) 46 | 47 | -------------------------------------------------------------------------------- /SwiftSemanticSearch.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 46B4675E2BD70804009D6420 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46B4675D2BD70804009D6420 /* SearchModel.swift */; }; 11 | A208E9DC2BCB3A20005F91B5 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A208E9DB2BCB3A20005F91B5 /* App.swift */; }; 12 | A208E9DE2BCB3A20005F91B5 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A208E9DD2BCB3A20005F91B5 /* ContentView.swift */; }; 13 | A208E9E02BCB3A21005F91B5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A208E9DF2BCB3A21005F91B5 /* Assets.xcassets */; }; 14 | A208E9E32BCB3A21005F91B5 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A208E9E22BCB3A21005F91B5 /* Preview Assets.xcassets */; }; 15 | A2672E2F2BE02AB1001606F8 /* Media in Frameworks */ = {isa = PBXBuildFile; productRef = A2672E2E2BE02AB1001606F8 /* Media */; }; 16 | A297E7C42BD6FFCA00C478C2 /* UForm in Frameworks */ = {isa = PBXBuildFile; productRef = A297E7C32BD6FFCA00C478C2 /* UForm */; }; 17 | A2EABA6C2BCB4C8500CB4341 /* images.uform3-image-text-english-small.fbin in Resources */ = {isa = PBXBuildFile; fileRef = A2EABA692BCB4C8500CB4341 /* images.uform3-image-text-english-small.fbin */; }; 18 | A2EABA6E2BCB4C8500CB4341 /* images.names.txt in Resources */ = {isa = PBXBuildFile; fileRef = A2EABA6B2BCB4C8500CB4341 /* images.names.txt */; }; 19 | A2EABA6F2BCB4F7700CB4341 /* images in Resources */ = {isa = PBXBuildFile; fileRef = A2EABA6A2BCB4C8500CB4341 /* images */; }; 20 | A2EABA722BCB50CE00CB4341 /* USearch in Frameworks */ = {isa = PBXBuildFile; productRef = A2EABA712BCB50CE00CB4341 /* USearch */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXFileReference section */ 24 | 46120B1C2BDFD962007746C2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 25 | 46B4675D2BD70804009D6420 /* SearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = ""; }; 26 | A208E9D82BCB3A20005F91B5 /* SwiftSemanticSearch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftSemanticSearch.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | A208E9DB2BCB3A20005F91B5 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 28 | A208E9DD2BCB3A20005F91B5 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 29 | A208E9DF2BCB3A21005F91B5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | A208E9E22BCB3A21005F91B5 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 31 | A2EABA692BCB4C8500CB4341 /* images.uform3-image-text-english-small.fbin */ = {isa = PBXFileReference; lastKnownFileType = file; path = "images.uform3-image-text-english-small.fbin"; sourceTree = ""; }; 32 | A2EABA6A2BCB4C8500CB4341 /* images */ = {isa = PBXFileReference; lastKnownFileType = folder; path = images; sourceTree = ""; }; 33 | A2EABA6B2BCB4C8500CB4341 /* images.names.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = images.names.txt; sourceTree = ""; }; 34 | /* End PBXFileReference section */ 35 | 36 | /* Begin PBXFrameworksBuildPhase section */ 37 | A208E9D52BCB3A20005F91B5 /* Frameworks */ = { 38 | isa = PBXFrameworksBuildPhase; 39 | buildActionMask = 2147483647; 40 | files = ( 41 | A2672E2F2BE02AB1001606F8 /* Media in Frameworks */, 42 | A297E7C42BD6FFCA00C478C2 /* UForm in Frameworks */, 43 | A2EABA722BCB50CE00CB4341 /* USearch in Frameworks */, 44 | ); 45 | runOnlyForDeploymentPostprocessing = 0; 46 | }; 47 | /* End PBXFrameworksBuildPhase section */ 48 | 49 | /* Begin PBXGroup section */ 50 | A208E9CF2BCB3A20005F91B5 = { 51 | isa = PBXGroup; 52 | children = ( 53 | A2EABA6A2BCB4C8500CB4341 /* images */, 54 | A2EABA6B2BCB4C8500CB4341 /* images.names.txt */, 55 | A2EABA692BCB4C8500CB4341 /* images.uform3-image-text-english-small.fbin */, 56 | A208E9DA2BCB3A20005F91B5 /* SwiftSemanticSearch */, 57 | A208E9D92BCB3A20005F91B5 /* Products */, 58 | A2236C0A2BCB602400449368 /* Frameworks */, 59 | ); 60 | sourceTree = ""; 61 | }; 62 | A208E9D92BCB3A20005F91B5 /* Products */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | A208E9D82BCB3A20005F91B5 /* SwiftSemanticSearch.app */, 66 | ); 67 | name = Products; 68 | sourceTree = ""; 69 | }; 70 | A208E9DA2BCB3A20005F91B5 /* SwiftSemanticSearch */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 46120B1C2BDFD962007746C2 /* Info.plist */, 74 | A208E9DD2BCB3A20005F91B5 /* ContentView.swift */, 75 | 46B4675D2BD70804009D6420 /* SearchModel.swift */, 76 | A208E9DB2BCB3A20005F91B5 /* App.swift */, 77 | A208E9DF2BCB3A21005F91B5 /* Assets.xcassets */, 78 | A208E9E12BCB3A21005F91B5 /* Preview Content */, 79 | ); 80 | path = SwiftSemanticSearch; 81 | sourceTree = ""; 82 | }; 83 | A208E9E12BCB3A21005F91B5 /* Preview Content */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | A208E9E22BCB3A21005F91B5 /* Preview Assets.xcassets */, 87 | ); 88 | path = "Preview Content"; 89 | sourceTree = ""; 90 | }; 91 | A2236C0A2BCB602400449368 /* Frameworks */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | ); 95 | name = Frameworks; 96 | sourceTree = ""; 97 | }; 98 | /* End PBXGroup section */ 99 | 100 | /* Begin PBXNativeTarget section */ 101 | A208E9D72BCB3A20005F91B5 /* SwiftSemanticSearch */ = { 102 | isa = PBXNativeTarget; 103 | buildConfigurationList = A208E9E62BCB3A21005F91B5 /* Build configuration list for PBXNativeTarget "SwiftSemanticSearch" */; 104 | buildPhases = ( 105 | A208E9D42BCB3A20005F91B5 /* Sources */, 106 | A208E9D52BCB3A20005F91B5 /* Frameworks */, 107 | A208E9D62BCB3A20005F91B5 /* Resources */, 108 | ); 109 | buildRules = ( 110 | ); 111 | dependencies = ( 112 | ); 113 | name = SwiftSemanticSearch; 114 | packageProductDependencies = ( 115 | A2EABA712BCB50CE00CB4341 /* USearch */, 116 | A297E7C32BD6FFCA00C478C2 /* UForm */, 117 | A2672E2E2BE02AB1001606F8 /* Media */, 118 | ); 119 | productName = SwiftSemanticSearch; 120 | productReference = A208E9D82BCB3A20005F91B5 /* SwiftSemanticSearch.app */; 121 | productType = "com.apple.product-type.application"; 122 | }; 123 | /* End PBXNativeTarget section */ 124 | 125 | /* Begin PBXProject section */ 126 | A208E9D02BCB3A20005F91B5 /* Project object */ = { 127 | isa = PBXProject; 128 | attributes = { 129 | BuildIndependentTargetsInParallel = 1; 130 | LastSwiftUpdateCheck = 1520; 131 | LastUpgradeCheck = 1520; 132 | TargetAttributes = { 133 | A208E9D72BCB3A20005F91B5 = { 134 | CreatedOnToolsVersion = 15.2; 135 | }; 136 | }; 137 | }; 138 | buildConfigurationList = A208E9D32BCB3A20005F91B5 /* Build configuration list for PBXProject "SwiftSemanticSearch" */; 139 | compatibilityVersion = "Xcode 14.0"; 140 | developmentRegion = en; 141 | hasScannedForEncodings = 0; 142 | knownRegions = ( 143 | en, 144 | Base, 145 | ); 146 | mainGroup = A208E9CF2BCB3A20005F91B5; 147 | packageReferences = ( 148 | A2EABA702BCB50CE00CB4341 /* XCRemoteSwiftPackageReference "usearch" */, 149 | A297E7C22BD6FFCA00C478C2 /* XCRemoteSwiftPackageReference "uform" */, 150 | 46B4675A2BD707EF009D6420 /* XCRemoteSwiftPackageReference "SwiftUIX" */, 151 | 463083262BDCAA2B006A644E /* XCRemoteSwiftPackageReference "Media" */, 152 | ); 153 | productRefGroup = A208E9D92BCB3A20005F91B5 /* Products */; 154 | projectDirPath = ""; 155 | projectRoot = ""; 156 | targets = ( 157 | A208E9D72BCB3A20005F91B5 /* SwiftSemanticSearch */, 158 | ); 159 | }; 160 | /* End PBXProject section */ 161 | 162 | /* Begin PBXResourcesBuildPhase section */ 163 | A208E9D62BCB3A20005F91B5 /* Resources */ = { 164 | isa = PBXResourcesBuildPhase; 165 | buildActionMask = 2147483647; 166 | files = ( 167 | A208E9E32BCB3A21005F91B5 /* Preview Assets.xcassets in Resources */, 168 | A208E9E02BCB3A21005F91B5 /* Assets.xcassets in Resources */, 169 | A2EABA6E2BCB4C8500CB4341 /* images.names.txt in Resources */, 170 | A2EABA6C2BCB4C8500CB4341 /* images.uform3-image-text-english-small.fbin in Resources */, 171 | A2EABA6F2BCB4F7700CB4341 /* images in Resources */, 172 | ); 173 | runOnlyForDeploymentPostprocessing = 0; 174 | }; 175 | /* End PBXResourcesBuildPhase section */ 176 | 177 | /* Begin PBXSourcesBuildPhase section */ 178 | A208E9D42BCB3A20005F91B5 /* Sources */ = { 179 | isa = PBXSourcesBuildPhase; 180 | buildActionMask = 2147483647; 181 | files = ( 182 | A208E9DE2BCB3A20005F91B5 /* ContentView.swift in Sources */, 183 | 46B4675E2BD70804009D6420 /* SearchModel.swift in Sources */, 184 | A208E9DC2BCB3A20005F91B5 /* App.swift in Sources */, 185 | ); 186 | runOnlyForDeploymentPostprocessing = 0; 187 | }; 188 | /* End PBXSourcesBuildPhase section */ 189 | 190 | /* Begin XCBuildConfiguration section */ 191 | A208E9E42BCB3A21005F91B5 /* Debug */ = { 192 | isa = XCBuildConfiguration; 193 | buildSettings = { 194 | ALWAYS_SEARCH_USER_PATHS = NO; 195 | APP_SHORTCUTS_ENABLE_FLEXIBLE_MATCHING = NO; 196 | ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; 197 | ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = ""; 198 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; 199 | CLANG_ANALYZER_NONNULL = YES; 200 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 201 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 202 | CLANG_ENABLE_MODULES = YES; 203 | CLANG_ENABLE_OBJC_ARC = YES; 204 | CLANG_ENABLE_OBJC_WEAK = YES; 205 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 206 | CLANG_WARN_BOOL_CONVERSION = YES; 207 | CLANG_WARN_COMMA = YES; 208 | CLANG_WARN_CONSTANT_CONVERSION = YES; 209 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 210 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 211 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 212 | CLANG_WARN_EMPTY_BODY = YES; 213 | CLANG_WARN_ENUM_CONVERSION = YES; 214 | CLANG_WARN_INFINITE_RECURSION = YES; 215 | CLANG_WARN_INT_CONVERSION = YES; 216 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 217 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 218 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 219 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 220 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 221 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 222 | CLANG_WARN_STRICT_PROTOTYPES = YES; 223 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 224 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 225 | CLANG_WARN_UNREACHABLE_CODE = YES; 226 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 227 | COPY_PHASE_STRIP = NO; 228 | DEBUG_INFORMATION_FORMAT = dwarf; 229 | ENABLE_STRICT_OBJC_MSGSEND = YES; 230 | ENABLE_TESTABILITY = YES; 231 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 232 | GCC_C_LANGUAGE_STANDARD = gnu17; 233 | GCC_DYNAMIC_NO_PIC = NO; 234 | GCC_NO_COMMON_BLOCKS = YES; 235 | GCC_OPTIMIZATION_LEVEL = 0; 236 | GCC_PREPROCESSOR_DEFINITIONS = ( 237 | "DEBUG=1", 238 | "$(inherited)", 239 | ); 240 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 241 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 242 | GCC_WARN_UNDECLARED_SELECTOR = YES; 243 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 244 | GCC_WARN_UNUSED_FUNCTION = YES; 245 | GCC_WARN_UNUSED_VARIABLE = YES; 246 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 247 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 248 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 249 | MTL_FAST_MATH = YES; 250 | ONLY_ACTIVE_ARCH = YES; 251 | SDKROOT = iphoneos; 252 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 253 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 254 | }; 255 | name = Debug; 256 | }; 257 | A208E9E52BCB3A21005F91B5 /* Release */ = { 258 | isa = XCBuildConfiguration; 259 | buildSettings = { 260 | ALWAYS_SEARCH_USER_PATHS = NO; 261 | APP_SHORTCUTS_ENABLE_FLEXIBLE_MATCHING = NO; 262 | ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; 263 | ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = ""; 264 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; 265 | CLANG_ANALYZER_NONNULL = YES; 266 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 267 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 268 | CLANG_ENABLE_MODULES = YES; 269 | CLANG_ENABLE_OBJC_ARC = YES; 270 | CLANG_ENABLE_OBJC_WEAK = YES; 271 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 272 | CLANG_WARN_BOOL_CONVERSION = YES; 273 | CLANG_WARN_COMMA = YES; 274 | CLANG_WARN_CONSTANT_CONVERSION = YES; 275 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 276 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 277 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 278 | CLANG_WARN_EMPTY_BODY = YES; 279 | CLANG_WARN_ENUM_CONVERSION = YES; 280 | CLANG_WARN_INFINITE_RECURSION = YES; 281 | CLANG_WARN_INT_CONVERSION = YES; 282 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 283 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 284 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 285 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 286 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 287 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 288 | CLANG_WARN_STRICT_PROTOTYPES = YES; 289 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 290 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 291 | CLANG_WARN_UNREACHABLE_CODE = YES; 292 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 293 | COPY_PHASE_STRIP = NO; 294 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 295 | ENABLE_NS_ASSERTIONS = NO; 296 | ENABLE_STRICT_OBJC_MSGSEND = YES; 297 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 298 | GCC_C_LANGUAGE_STANDARD = gnu17; 299 | GCC_NO_COMMON_BLOCKS = YES; 300 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 301 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 302 | GCC_WARN_UNDECLARED_SELECTOR = YES; 303 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 304 | GCC_WARN_UNUSED_FUNCTION = YES; 305 | GCC_WARN_UNUSED_VARIABLE = YES; 306 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 307 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 308 | MTL_ENABLE_DEBUG_INFO = NO; 309 | MTL_FAST_MATH = YES; 310 | SDKROOT = iphoneos; 311 | SWIFT_COMPILATION_MODE = wholemodule; 312 | VALIDATE_PRODUCT = YES; 313 | }; 314 | name = Release; 315 | }; 316 | A208E9E72BCB3A21005F91B5 /* Debug */ = { 317 | isa = XCBuildConfiguration; 318 | buildSettings = { 319 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 320 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 321 | CODE_SIGN_STYLE = Automatic; 322 | CURRENT_PROJECT_VERSION = 1; 323 | DEVELOPMENT_ASSET_PATHS = "\"SwiftSemanticSearch/Preview Content\""; 324 | DEVELOPMENT_TEAM = 4J62EE2G74; 325 | ENABLE_PREVIEWS = YES; 326 | GENERATE_INFOPLIST_FILE = YES; 327 | INFOPLIST_FILE = SwiftSemanticSearch/Info.plist; 328 | INFOPLIST_KEY_CFBundleDisplayName = "Swift Semantic Search"; 329 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography"; 330 | INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera usage."; 331 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 332 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 333 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 334 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 335 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 336 | LD_RUNPATH_SEARCH_PATHS = ( 337 | "$(inherited)", 338 | "@executable_path/Frameworks", 339 | ); 340 | MARKETING_VERSION = 1.0; 341 | PRODUCT_BUNDLE_IDENTIFIER = SwiftSemanticSearch; 342 | PRODUCT_NAME = "$(TARGET_NAME)"; 343 | SWIFT_EMIT_LOC_STRINGS = YES; 344 | SWIFT_VERSION = 5.0; 345 | TARGETED_DEVICE_FAMILY = "1,2"; 346 | }; 347 | name = Debug; 348 | }; 349 | A208E9E82BCB3A21005F91B5 /* Release */ = { 350 | isa = XCBuildConfiguration; 351 | buildSettings = { 352 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 353 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 354 | CODE_SIGN_STYLE = Automatic; 355 | CURRENT_PROJECT_VERSION = 1; 356 | DEVELOPMENT_ASSET_PATHS = "\"SwiftSemanticSearch/Preview Content\""; 357 | DEVELOPMENT_TEAM = 4J62EE2G74; 358 | ENABLE_PREVIEWS = YES; 359 | GENERATE_INFOPLIST_FILE = YES; 360 | INFOPLIST_FILE = SwiftSemanticSearch/Info.plist; 361 | INFOPLIST_KEY_CFBundleDisplayName = "Swift Semantic Search"; 362 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography"; 363 | INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera usage."; 364 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 365 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 366 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 367 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 368 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 369 | LD_RUNPATH_SEARCH_PATHS = ( 370 | "$(inherited)", 371 | "@executable_path/Frameworks", 372 | ); 373 | MARKETING_VERSION = 1.0; 374 | PRODUCT_BUNDLE_IDENTIFIER = SwiftSemanticSearch; 375 | PRODUCT_NAME = "$(TARGET_NAME)"; 376 | SWIFT_EMIT_LOC_STRINGS = YES; 377 | SWIFT_VERSION = 5.0; 378 | TARGETED_DEVICE_FAMILY = "1,2"; 379 | }; 380 | name = Release; 381 | }; 382 | /* End XCBuildConfiguration section */ 383 | 384 | /* Begin XCConfigurationList section */ 385 | A208E9D32BCB3A20005F91B5 /* Build configuration list for PBXProject "SwiftSemanticSearch" */ = { 386 | isa = XCConfigurationList; 387 | buildConfigurations = ( 388 | A208E9E42BCB3A21005F91B5 /* Debug */, 389 | A208E9E52BCB3A21005F91B5 /* Release */, 390 | ); 391 | defaultConfigurationIsVisible = 0; 392 | defaultConfigurationName = Release; 393 | }; 394 | A208E9E62BCB3A21005F91B5 /* Build configuration list for PBXNativeTarget "SwiftSemanticSearch" */ = { 395 | isa = XCConfigurationList; 396 | buildConfigurations = ( 397 | A208E9E72BCB3A21005F91B5 /* Debug */, 398 | A208E9E82BCB3A21005F91B5 /* Release */, 399 | ); 400 | defaultConfigurationIsVisible = 0; 401 | defaultConfigurationName = Release; 402 | }; 403 | /* End XCConfigurationList section */ 404 | 405 | /* Begin XCRemoteSwiftPackageReference section */ 406 | 463083262BDCAA2B006A644E /* XCRemoteSwiftPackageReference "Media" */ = { 407 | isa = XCRemoteSwiftPackageReference; 408 | repositoryURL = "https://github.com/vmanot/Media.git"; 409 | requirement = { 410 | branch = main; 411 | kind = branch; 412 | }; 413 | }; 414 | 46B4675A2BD707EF009D6420 /* XCRemoteSwiftPackageReference "SwiftUIX" */ = { 415 | isa = XCRemoteSwiftPackageReference; 416 | repositoryURL = "https://github.com/SwiftUIX/SwiftUIX.git"; 417 | requirement = { 418 | branch = master; 419 | kind = branch; 420 | }; 421 | }; 422 | A297E7C22BD6FFCA00C478C2 /* XCRemoteSwiftPackageReference "uform" */ = { 423 | isa = XCRemoteSwiftPackageReference; 424 | repositoryURL = "https://github.com/unum-cloud/uform/"; 425 | requirement = { 426 | branch = main; 427 | kind = branch; 428 | }; 429 | }; 430 | A2EABA702BCB50CE00CB4341 /* XCRemoteSwiftPackageReference "usearch" */ = { 431 | isa = XCRemoteSwiftPackageReference; 432 | repositoryURL = "https://github.com/unum-cloud/usearch"; 433 | requirement = { 434 | branch = main; 435 | kind = branch; 436 | }; 437 | }; 438 | /* End XCRemoteSwiftPackageReference section */ 439 | 440 | /* Begin XCSwiftPackageProductDependency section */ 441 | A2672E2E2BE02AB1001606F8 /* Media */ = { 442 | isa = XCSwiftPackageProductDependency; 443 | productName = Media; 444 | }; 445 | A297E7C32BD6FFCA00C478C2 /* UForm */ = { 446 | isa = XCSwiftPackageProductDependency; 447 | package = A297E7C22BD6FFCA00C478C2 /* XCRemoteSwiftPackageReference "uform" */; 448 | productName = UForm; 449 | }; 450 | A2EABA712BCB50CE00CB4341 /* USearch */ = { 451 | isa = XCSwiftPackageProductDependency; 452 | package = A2EABA702BCB50CE00CB4341 /* XCRemoteSwiftPackageReference "usearch" */; 453 | productName = USearch; 454 | }; 455 | /* End XCSwiftPackageProductDependency section */ 456 | }; 457 | rootObject = A208E9D02BCB3A20005F91B5 /* Project object */; 458 | } 459 | -------------------------------------------------------------------------------- /SwiftSemanticSearch.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftSemanticSearch.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftSemanticSearch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "f89a9ca8981fb47416f633fc83ba5bd401d3d0d96324c9c4de94435810bc17dc", 3 | "pins" : [ 4 | { 5 | "identity" : "corepersistence", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/vmanot/CorePersistence.git", 8 | "state" : { 9 | "branch" : "main", 10 | "revision" : "38fd5271fa906a2d8395e4b42724142886a3c763" 11 | } 12 | }, 13 | { 14 | "identity" : "media", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/vmanot/Media.git", 17 | "state" : { 18 | "branch" : "main", 19 | "revision" : "0ba2baaebeb58667955daef68d3535ba1b217a12" 20 | } 21 | }, 22 | { 23 | "identity" : "merge", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/vmanot/Merge.git", 26 | "state" : { 27 | "branch" : "master", 28 | "revision" : "e8bc37c8dc203cab481efedd71237c151882c007" 29 | } 30 | }, 31 | { 32 | "identity" : "swallow", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/vmanot/Swallow.git", 35 | "state" : { 36 | "branch" : "master", 37 | "revision" : "6227a1114e341daf54e90df61e173599b187a9b1" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-argument-parser", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/apple/swift-argument-parser.git", 44 | "state" : { 45 | "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", 46 | "version" : "1.3.0" 47 | } 48 | }, 49 | { 50 | "identity" : "swift-collections", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/apple/swift-collections", 53 | "state" : { 54 | "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", 55 | "version" : "1.1.4" 56 | } 57 | }, 58 | { 59 | "identity" : "swift-syntax", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/apple/swift-syntax.git", 62 | "state" : { 63 | "revision" : "2bc86522d115234d1f588efe2bcb4ce4be8f8b82", 64 | "version" : "510.0.3" 65 | } 66 | }, 67 | { 68 | "identity" : "swift-transformers", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/ashvardanian/swift-transformers", 71 | "state" : { 72 | "revision" : "89fb5d97e1df347f9f588f62fc538dcad6fdb16c" 73 | } 74 | }, 75 | { 76 | "identity" : "swiftui-introspect", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/siteline/SwiftUI-Introspect", 79 | "state" : { 80 | "revision" : "121c146fe591b1320238d054ae35c81ffa45f45a", 81 | "version" : "0.12.0" 82 | } 83 | }, 84 | { 85 | "identity" : "swiftuix", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/SwiftUIX/SwiftUIX.git", 88 | "state" : { 89 | "branch" : "master", 90 | "revision" : "836fc284a9bb07fc9ab6d2dce6ebd0e32aabde26" 91 | } 92 | }, 93 | { 94 | "identity" : "swiftuiz", 95 | "kind" : "remoteSourceControl", 96 | "location" : "https://github.com/SwiftUIX/SwiftUIZ.git", 97 | "state" : { 98 | "branch" : "main", 99 | "revision" : "4260778caf919fddb992d56d4235de2e030d0672" 100 | } 101 | }, 102 | { 103 | "identity" : "uform", 104 | "kind" : "remoteSourceControl", 105 | "location" : "https://github.com/unum-cloud/uform/", 106 | "state" : { 107 | "branch" : "main", 108 | "revision" : "7b79dbdd9a83cd8931590951a5dab841ec938818" 109 | } 110 | }, 111 | { 112 | "identity" : "usearch", 113 | "kind" : "remoteSourceControl", 114 | "location" : "https://github.com/unum-cloud/usearch", 115 | "state" : { 116 | "branch" : "main", 117 | "revision" : "e7140e55967a4198f2bbd34987fa7597ebafa649" 118 | } 119 | } 120 | ], 121 | "version" : 3 122 | } 123 | -------------------------------------------------------------------------------- /SwiftSemanticSearch.xcodeproj/xcshareddata/xcschemes/SwiftSemanticSearch.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 44 | 46 | 52 | 53 | 54 | 55 | 61 | 63 | 69 | 70 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /SwiftSemanticSearch/App.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Ash Vardanian 3 | // 4 | 5 | import SwiftUI 6 | 7 | @main 8 | struct SwiftSemanticSearchApp: App { 9 | @StateObject var searchModel = SearchModel() 10 | 11 | var body: some Scene { 12 | WindowGroup { 13 | 14 | ContentView() 15 | .environmentObject(searchModel) 16 | .task { 17 | Task.detached(priority: .userInitiated) { 18 | await searchModel.loadEncodersAndIndexConcurrently() 19 | } 20 | } 21 | } 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /SwiftSemanticSearch/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SwiftSemanticSearch/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "SwiftSemanticSearch.jpg", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /SwiftSemanticSearch/Assets.xcassets/AppIcon.appiconset/SwiftSemanticSearch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashvardanian/SwiftSemanticSearch/6f1f79166aa9dfcca2073a75a167267b593781bd/SwiftSemanticSearch/Assets.xcassets/AppIcon.appiconset/SwiftSemanticSearch.jpg -------------------------------------------------------------------------------- /SwiftSemanticSearch/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftSemanticSearch/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Ash Vardanian 3 | // 4 | 5 | import Media // For camera - https://github.com/vmanot/Media 6 | import SwiftUIX // For debounce - https://github.com/vmanot/SwiftUIX 7 | 8 | enum SearchMode: String, CaseIterable, Codable, Hashable, Sendable { 9 | case text, videoStream, image 10 | } 11 | 12 | struct ContentView: View { 13 | @EnvironmentObject var searchModel: SearchModel 14 | @State private var searchText: String = "" 15 | @State private var searchImage: UIImage? = nil 16 | @State private var searchMode: SearchMode = .text 17 | /// The asynchronous function to be executed in the background 18 | @State private var searchTask: Task? 19 | /// Paths (or names) of images matching the current search query 20 | @State private var filteredData: [String] = [] 21 | 22 | var body: some View { 23 | NavigationView { 24 | VStack(spacing: 0) { 25 | contentForCurrentMode 26 | .clipped() 27 | searchResultsView 28 | .clipped() 29 | } 30 | .navigationBarTitle("Unum ❤️ Apple", displayMode: .inline) 31 | .toolbar { 32 | searchModeToolbar 33 | } 34 | .onAppear { 35 | showAll() 36 | } 37 | } 38 | .navigationViewStyle(.stack) 39 | } 40 | 41 | @ViewBuilder 42 | private var contentForCurrentMode: some View { 43 | switch searchMode { 44 | case .text: 45 | textFieldView 46 | case .videoStream, .image: 47 | MediaInputView( 48 | searchMode: searchMode, 49 | onImageCapture: { image in 50 | showSimilar(toImage: image) 51 | }, 52 | searchImage: searchImage, 53 | onImageTap: { 54 | showAll() 55 | 56 | searchMode = .text 57 | } 58 | ) 59 | } 60 | } 61 | 62 | private var textFieldView: some View { 63 | TextField("Search with UForm & USearch", text: $searchText) 64 | .padding() 65 | .multilineTextAlignment(.center) 66 | .alignmentGuide(HorizontalAlignment.center) 67 | } 68 | 69 | private var searchModeToolbar: some ToolbarContent { 70 | Group { 71 | ToolbarItem(placement: .navigationBarTrailing) { 72 | Button(action: toggleSearchMode) { 73 | Image(systemName: iconForCurrentMode) 74 | } 75 | } 76 | 77 | ToolbarItem(placement: .navigationBarLeading) { 78 | progressIndicator 79 | } 80 | } 81 | } 82 | 83 | @ViewBuilder 84 | private var progressIndicator: some View { 85 | if !searchModel.state.isSuperset(of: [.readyToSearch, .readyToShow]) { 86 | ProgressView().progressViewStyle(.circular) 87 | } 88 | } 89 | 90 | private var iconForCurrentMode: String { 91 | switch searchMode { 92 | case .text: return "camera" 93 | case .videoStream: return "photo" 94 | case .image: return "text.bubble" 95 | } 96 | } 97 | 98 | private var searchResultsView: some View { 99 | GeometryReader(alignment: .center) { geometry in 100 | SearchResultsView(geometry: geometry, data: filteredData, onTap: { image in 101 | showSimilar(toImage: image) 102 | searchMode = .image 103 | }) 104 | } 105 | .withChangePublisher(for: searchText) { publisher in 106 | publisher 107 | .receive(on: DispatchQueue.main) 108 | .handleEvents(receiveOutput: { _ in 109 | self.filteredData = [] 110 | }) 111 | .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) 112 | .sink { searchText in 113 | withAnimation(.default) { 114 | showSimilar(toText: searchText) 115 | } 116 | } 117 | } 118 | } 119 | } 120 | 121 | extension ContentView { 122 | private func toggleSearchMode() { 123 | switch searchMode { 124 | case .text: 125 | searchMode = .videoStream 126 | case .videoStream, .image: 127 | searchMode = .text 128 | } 129 | showAll() 130 | } 131 | 132 | private func showAll() { 133 | showSimilar() 134 | } 135 | 136 | private func showSimilar(toText query: String = "") { 137 | searchTask?.cancel() 138 | searchText = query 139 | searchTask = Task { 140 | let result = try await searchModel.filter(withText: query) 141 | try Task.checkCancellation() 142 | self.filteredData = result 143 | } 144 | } 145 | 146 | @MainActor 147 | private func showSimilar(toImage image: UIImage) { 148 | guard let cgImage = image.cgImage else { 149 | // Handle the error: no CGImage found 150 | print("No CGImage available in the UIImage") 151 | return 152 | } 153 | 154 | searchTask?.cancel() 155 | searchImage = image 156 | searchTask = Task.detached(priority: .high) { 157 | let result = try await searchModel.filter(withImage: cgImage) 158 | 159 | try Task.checkCancellation() 160 | 161 | await MainActor.run { 162 | self.filteredData = result 163 | } 164 | } 165 | } 166 | } 167 | 168 | extension ContentView { 169 | fileprivate struct MediaInputView: View { 170 | let searchMode: SearchMode 171 | let onImageCapture: (AppKitOrUIKitImage) -> Void 172 | let searchImage: AppKitOrUIKitImage? 173 | let onImageTap: () -> Void 174 | 175 | @State private var autoCapture: Bool = true 176 | 177 | var body: some View { 178 | GeometryReader(alignment: .center) { geo in 179 | let size = min(geo.size.width, geo.size.height) 180 | switch searchMode { 181 | case .videoStream: 182 | makeCameraView(size: size) 183 | case .image: 184 | makeImageView(size: size) 185 | default: 186 | EmptyView() 187 | } 188 | } 189 | } 190 | 191 | private func makeCameraView(size: CGFloat) -> some View { 192 | CameraViewReader { (camera: CameraViewProxy) in 193 | CameraView(camera: .back, mirrored: false) 194 | .aspectRatio(1.0, contentMode: .fill) 195 | .processingFrameRate(.fps1) 196 | .frame(width: .greedy, height: size) 197 | .onReceive(camera._outputImageBufferPublisher?.receiveOnMainQueue()) { cvImage in 198 | Task { @MainActor in 199 | guard autoCapture, let image = cvImage._cgImage else { 200 | return 201 | } 202 | 203 | self.onImageCapture(AppKitOrUIKitImage(cgImage: image)) 204 | } 205 | } 206 | } 207 | } 208 | 209 | @ViewBuilder 210 | private func makeImageView(size: CGFloat) -> some View { 211 | if let searchImage { 212 | Image(image: searchImage) 213 | .resizable() 214 | .aspectRatio(contentMode: .fill) 215 | .background(Color.almostClear) 216 | .contentShape(Rectangle()) 217 | .onTapGesture { 218 | onImageTap() 219 | } 220 | } 221 | } 222 | 223 | @ViewBuilder 224 | private func makeCaptureButton( 225 | camera: CameraViewProxy 226 | ) -> some View { 227 | Button { 228 | Task { @MainActor in 229 | do { 230 | autoCapture = false 231 | 232 | let image: AppKitOrUIKitImage = try await camera.capturePhoto() 233 | 234 | onImageCapture(image) 235 | } catch { 236 | runtimeIssue(error) 237 | } 238 | } 239 | } label: { 240 | Label { 241 | Text("Capture Photo") 242 | } icon: { 243 | Image(systemName: .cameraFill) 244 | } 245 | .font(.title2) 246 | .controlSize(.large) 247 | .padding(.small) 248 | } 249 | .buttonStyle(.borderedProminent) 250 | } 251 | } 252 | 253 | fileprivate struct SearchResultsView: View { 254 | let geometry: GeometryProxy 255 | let data: [String] 256 | let onTap: (AppKitOrUIKitImage) -> Void 257 | 258 | var body: some View { 259 | let imageOptimalWidth = 250.0 260 | let minColumns = 3 261 | 262 | // Dynamically calculate the number of columns based on the available width, 263 | // so that the images are displayed in a grid layout without any empty space 264 | let numberOfColumns = Int(max(minColumns, Int(geometry.size.width / imageOptimalWidth))) 265 | let columns = Array(repeating: GridItem(.flexible(), spacing: 0), count: numberOfColumns) 266 | let width = geometry.size.width / CGFloat(numberOfColumns) 267 | 268 | ScrollView { 269 | LazyVGrid(columns: columns, spacing: 0) { 270 | ForEach(data, id: \.self) { imageName in 271 | if let imagePath = Bundle.main.path(forResource: imageName, ofType: nil, inDirectory: "images"), 272 | let image = UIImage(contentsOfFile: imagePath) { 273 | Image(uiImage: image) 274 | .resizable() 275 | .scaledToFit() 276 | .frame(width: width, height: width) 277 | .onTapGesture { 278 | onTap(image) 279 | } 280 | } 281 | } 282 | 283 | } 284 | 285 | } 286 | } 287 | } 288 | } 289 | 290 | #Preview { 291 | ContentView() 292 | } 293 | 294 | -------------------------------------------------------------------------------- /SwiftSemanticSearch/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /SwiftSemanticSearch/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftSemanticSearch/SearchModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Ash Vardanian 3 | // 4 | 5 | import Accelerate 6 | import Combine 7 | import SwiftUI 8 | import UForm 9 | import USearch 10 | 11 | class SearchModel: ObservableObject { 12 | @MainActor 13 | public enum StateFlag { 14 | case readyToShow 15 | case readyToSearch 16 | } 17 | 18 | @MainActor 19 | @Published 20 | public var state: Set = [] 21 | 22 | private var _loadEncodersAndIndexConcurrentlyTask: Task? = nil 23 | private var allImageNames: [String] = [] 24 | private var textEncoder: TextEncoder? 25 | private var imageEncoder: ImageEncoder? 26 | 27 | fileprivate var imageIndex: USearchIndex? 28 | 29 | private lazy var textEncoderActor = TextEncoderActor() 30 | private lazy var imageEncoderActor = ImageEncoderActor() 31 | private lazy var imageIndexActor = ImageIndexActor(searchModel: self) 32 | 33 | private var rows: UInt32 = 0 34 | private var columns: UInt32 = 0 35 | private var matrix: [Float] = [] 36 | 37 | init() { 38 | allImageNames = loadImageNames() 39 | let persistedImageFilename = listFilesInImagesFolder() 40 | checkForMissingImages(imageNames: allImageNames, imageFiles: persistedImageFilename) 41 | loadMatrix() 42 | 43 | Task.detached(priority: .userInitiated) { 44 | await self.loadEncodersAndIndexConcurrently() 45 | } 46 | } 47 | 48 | func loadImageNames() -> [String] { 49 | if let filepath = Bundle.main.resourcePath?.appending("/images.names.txt") { 50 | do { 51 | let contents = try String(contentsOfFile: filepath) 52 | var names = contents.components(separatedBy: "\n") 53 | names = names.filter { !$0.isEmpty } // Removing any empty lines 54 | names = names.map { $0 + ".jpg" } 55 | print("Contains \(names.count) files in \(filepath)") 56 | return names 57 | } catch { 58 | print("Error reading the contents of the file: \(error)") 59 | } 60 | } 61 | return [] 62 | } 63 | 64 | func listFilesInImagesFolder() -> [String] { 65 | if let imagesPath = Bundle.main.resourcePath?.appending("/images") { 66 | do { 67 | let fileManager = FileManager.default 68 | let imageFiles = try fileManager.contentsOfDirectory(atPath: imagesPath) 69 | print("Contains \(imageFiles.count) files in \(imagesPath)") 70 | return imageFiles 71 | } catch { 72 | print("Error while enumerating files in /images: \(error.localizedDescription)") 73 | } 74 | } 75 | return [] 76 | } 77 | 78 | func checkForMissingImages(imageNames: [String], imageFiles: [String]) { 79 | // Ensure loadImageNames() and listFilesInImagesFolder() have been called 80 | guard !imageNames.isEmpty && !imageFiles.isEmpty else { return } 81 | 82 | // Convert imageNames to just the filenames (in case they have path components) 83 | let imageNamesSet = Set(imageNames.map { URL(fileURLWithPath: $0).lastPathComponent }) 84 | 85 | // Convert imageFiles to a set 86 | let imageFilesSet = Set(imageFiles) 87 | 88 | // Find the difference 89 | let missingFiles = imageNamesSet.subtracting(imageFilesSet) 90 | if missingFiles.isEmpty { 91 | print("All files are present.") 92 | } else { 93 | print("Missing files: \(missingFiles)") 94 | } 95 | } 96 | 97 | func loadMatrix() { 98 | 99 | guard let filePath = Bundle.main.resourcePath?.appending("/images.uform3-image-text-english-small.fbin") else { 100 | print("Matrix file not found.") 101 | return 102 | } 103 | 104 | do { 105 | let data = try Data(contentsOf: URL(fileURLWithPath: filePath)) 106 | data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in 107 | var offset = 0 108 | // Extract rows and columns from the beginning of the file 109 | let rowsPointer = bytes.baseAddress!.assumingMemoryBound(to: UInt32.self) 110 | rows = rowsPointer.pointee 111 | offset += MemoryLayout.size 112 | 113 | let columnsPointer = bytes.baseAddress!.advanced(by: offset).assumingMemoryBound(to: UInt32.self) 114 | columns = columnsPointer.pointee 115 | offset += MemoryLayout.size 116 | 117 | let rawMatrix = UnsafeBufferPointer(start: bytes.baseAddress!.advanced(by: offset).assumingMemoryBound(to: Float.self), count: rows * columns) 118 | 119 | // Now that we know the size of the matrix, allocate it 120 | matrix = Array(rawMatrix) 121 | 122 | print("Loaded a \(rows) x \(columns) matrix") 123 | } 124 | } catch { 125 | print("Error loading matrix file: \(error)") 126 | } 127 | } 128 | 129 | @MainActor 130 | func loadEncodersAndIndexConcurrently() async { 131 | do { 132 | _ = state.insert(.readyToShow) 133 | 134 | _loadEncodersAndIndexConcurrentlyTask = Task.detached(priority: .userInitiated) { 135 | try await withThrowingTaskGroup(of: Void.self) { group in 136 | group.addTask(priority: .userInitiated) { 137 | try await self.textEncoderActor.load() 138 | } 139 | 140 | group.addTask(priority: .userInitiated) { 141 | try await self.imageEncoderActor.load() 142 | } 143 | 144 | group.addTask(priority: .userInitiated) { 145 | try await self.imageIndexActor.index( 146 | matrix: self.matrix, 147 | rows: self.rows, 148 | columns: self.columns 149 | ) 150 | } 151 | 152 | try await group.waitForAll() 153 | } 154 | } 155 | 156 | try await _loadEncodersAndIndexConcurrentlyTask?.value 157 | 158 | self.textEncoder = await self.textEncoderActor.textEncoder 159 | self.imageEncoder = await self.imageEncoderActor.imageEncoder 160 | 161 | _ = state.insert(.readyToSearch) 162 | } catch { 163 | assertionFailure(String(describing: error)) 164 | } 165 | } 166 | 167 | func filter(withText query: String) async throws -> [String] { 168 | 169 | if query.isEmpty { 170 | return allImageNames 171 | } 172 | 173 | print("Wants to filter images by \(query)") 174 | 175 | 176 | guard let textEncoder = textEncoder, let imageIndex = imageIndex else { 177 | return [] 178 | } 179 | 180 | do { 181 | // Get the embedding for the query. 182 | let queryEmbedding = try textEncoder.encode(query).asFloats() 183 | let results = imageIndex.search(vector: queryEmbedding, count: 100) 184 | 185 | await Task.yield() 186 | 187 | try Task.checkCancellation() 188 | 189 | // Calculate the cosine similarity of each image's embedding to the query's embedding. 190 | let similarityScores = zip(results.0, results.1).map { (key: USearchKey, similarity: Float32) in 191 | let imageName = allImageNames[Int(key)] 192 | return (imageName, similarity) 193 | } 194 | 195 | await Task.yield() 196 | 197 | try Task.checkCancellation() 198 | 199 | // Sort the images by descending similarity scores. 200 | return similarityScores 201 | .sorted { $0.1 < $1.1 } 202 | .map { $0.0 } 203 | 204 | } catch { 205 | print("Error processing embeddings: \(error)") 206 | return [] 207 | } 208 | } 209 | } 210 | 211 | extension SearchModel { 212 | @MainActor 213 | func filter( 214 | withImage query: CGImage 215 | ) async throws -> [String] { 216 | try await _loadEncodersAndIndexConcurrentlyTask?.value 217 | 218 | guard let imageEncoder = imageEncoder, let imageIndex = imageIndex else { 219 | return [] 220 | } 221 | 222 | do { 223 | // Get the embedding for the query. 224 | let queryEmbedding = try imageEncoder.encode(query).asFloats() 225 | let results = imageIndex.search(vector: queryEmbedding, count: 100) 226 | 227 | // Calculate the cosine similarity of each image's embedding to the query's embedding. 228 | let similarityScores = zip(results.0, results.1).map { (key: USearchKey, similarity: Float32) in 229 | let imageName = allImageNames[Int(key)] 230 | return (imageName, similarity) 231 | } 232 | 233 | // Sort the images by descending similarity scores. 234 | return similarityScores 235 | .sorted { $0.1 < $1.1 } 236 | .map { $0.0 } 237 | 238 | } catch { 239 | print("Error processing embeddings: \(error)") 240 | return [] 241 | } 242 | } 243 | } 244 | 245 | // MARK: - Auxiliary 246 | 247 | // Define separate actors for encapsulating each resource, that can operate concurrently 248 | actor TextEncoderActor { 249 | var textEncoder: TextEncoder? 250 | 251 | func load() async throws { 252 | self.textEncoder = try await TextEncoder( 253 | modelName: "unum-cloud/uform3-image-text-english-small", 254 | computeUnits: .cpuAndNeuralEngine 255 | ) 256 | } 257 | } 258 | 259 | actor ImageEncoderActor { 260 | var imageEncoder: ImageEncoder? 261 | 262 | func load() async throws { 263 | self.imageEncoder = try await ImageEncoder( 264 | modelName: "unum-cloud/uform3-image-text-english-small", 265 | computeUnits: .cpuAndNeuralEngine 266 | ) 267 | } 268 | } 269 | 270 | actor ImageIndexActor { 271 | private var searchModel: SearchModel 272 | 273 | init(searchModel: SearchModel) { 274 | self.searchModel = searchModel 275 | } 276 | 277 | @usableFromInline 278 | func index( 279 | matrix: [Float], 280 | rows: UInt32, 281 | columns: UInt32 282 | ) async throws { 283 | let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! 284 | let indexPathForSave = documentsDirectory.appendingPathComponent("images.uform3-image-text-english-small.usearch") 285 | 286 | let indexPath = Bundle.main.resourcePath!.appending("/images.uform3-image-text-english-small.usearch") 287 | let indexURL = URL(fileURLWithPath: indexPath) 288 | 289 | let imageIndex = USearchIndex.make( 290 | metric: .cos, 291 | dimensions: columns, 292 | connectivity: 0, 293 | quantization: .F16 294 | ) 295 | 296 | if FileManager.default.fileExists(at: indexURL) { 297 | print("Bundle index found. Load index from bundle file") 298 | imageIndex.load(path: indexPath) 299 | } 300 | else if FileManager.default.fileExists(at: indexPathForSave) { 301 | print("Saved index found. Load index from saved index file") 302 | imageIndex.load(path: indexPathForSave.path) 303 | } 304 | else { 305 | print("No bundle or saved index found. Build index and save it.") 306 | let _ = imageIndex.reserve(rows) 307 | 308 | let columns = Int(columns) 309 | 310 | await (0.. = Int(row * columns).., Element == Int) 327 | public func concurrentForEach( 328 | @_implicitSelfCapture _ operation: @escaping @Sendable (Element) async -> Void 329 | ) async { 330 | await withTaskGroup(of: Void.self) { group in 331 | for element in self { 332 | group.addTask { 333 | await operation(element) 334 | } 335 | } 336 | 337 | await group.waitForAll() 338 | } 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /SwiftVectorSearch.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A23025212A6D8DAC00184207 /* SwiftVectorSearchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A23025202A6D8DAC00184207 /* SwiftVectorSearchApp.swift */; }; 11 | A23025252A6D8DAD00184207 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A23025242A6D8DAD00184207 /* Assets.xcassets */; }; 12 | A23025282A6D8DAD00184207 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A23025272A6D8DAD00184207 /* Preview Assets.xcassets */; }; 13 | A23025302A6D8DED00184207 /* USearch in Frameworks */ = {isa = PBXBuildFile; productRef = A230252F2A6D8DED00184207 /* USearch */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | A230251D2A6D8DAC00184207 /* SwiftVectorSearch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftVectorSearch.app; sourceTree = BUILT_PRODUCTS_DIR; }; 18 | A23025202A6D8DAC00184207 /* SwiftVectorSearchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftVectorSearchApp.swift; sourceTree = ""; }; 19 | A23025242A6D8DAD00184207 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 20 | A23025272A6D8DAD00184207 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 21 | /* End PBXFileReference section */ 22 | 23 | /* Begin PBXFrameworksBuildPhase section */ 24 | A230251A2A6D8DAC00184207 /* Frameworks */ = { 25 | isa = PBXFrameworksBuildPhase; 26 | buildActionMask = 2147483647; 27 | files = ( 28 | A23025302A6D8DED00184207 /* USearch in Frameworks */, 29 | ); 30 | runOnlyForDeploymentPostprocessing = 0; 31 | }; 32 | /* End PBXFrameworksBuildPhase section */ 33 | 34 | /* Begin PBXGroup section */ 35 | A23025142A6D8DAC00184207 = { 36 | isa = PBXGroup; 37 | children = ( 38 | A230251F2A6D8DAC00184207 /* SwiftVectorSearch */, 39 | A230251E2A6D8DAC00184207 /* Products */, 40 | ); 41 | sourceTree = ""; 42 | }; 43 | A230251E2A6D8DAC00184207 /* Products */ = { 44 | isa = PBXGroup; 45 | children = ( 46 | A230251D2A6D8DAC00184207 /* SwiftVectorSearch.app */, 47 | ); 48 | name = Products; 49 | sourceTree = ""; 50 | }; 51 | A230251F2A6D8DAC00184207 /* SwiftVectorSearch */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | A23025202A6D8DAC00184207 /* SwiftVectorSearchApp.swift */, 55 | A23025242A6D8DAD00184207 /* Assets.xcassets */, 56 | A23025262A6D8DAD00184207 /* Preview Content */, 57 | ); 58 | path = SwiftVectorSearch; 59 | sourceTree = ""; 60 | }; 61 | A23025262A6D8DAD00184207 /* Preview Content */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | A23025272A6D8DAD00184207 /* Preview Assets.xcassets */, 65 | ); 66 | path = "Preview Content"; 67 | sourceTree = ""; 68 | }; 69 | /* End PBXGroup section */ 70 | 71 | /* Begin PBXNativeTarget section */ 72 | A230251C2A6D8DAC00184207 /* SwiftVectorSearch */ = { 73 | isa = PBXNativeTarget; 74 | buildConfigurationList = A230252B2A6D8DAD00184207 /* Build configuration list for PBXNativeTarget "SwiftVectorSearch" */; 75 | buildPhases = ( 76 | A23025192A6D8DAC00184207 /* Sources */, 77 | A230251A2A6D8DAC00184207 /* Frameworks */, 78 | A230251B2A6D8DAC00184207 /* Resources */, 79 | ); 80 | buildRules = ( 81 | ); 82 | dependencies = ( 83 | ); 84 | name = SwiftVectorSearch; 85 | packageProductDependencies = ( 86 | A230252F2A6D8DED00184207 /* USearch */, 87 | ); 88 | productName = SwiftVectorSearch; 89 | productReference = A230251D2A6D8DAC00184207 /* SwiftVectorSearch.app */; 90 | productType = "com.apple.product-type.application"; 91 | }; 92 | /* End PBXNativeTarget section */ 93 | 94 | /* Begin PBXProject section */ 95 | A23025152A6D8DAC00184207 /* Project object */ = { 96 | isa = PBXProject; 97 | attributes = { 98 | BuildIndependentTargetsInParallel = 1; 99 | LastSwiftUpdateCheck = 1430; 100 | LastUpgradeCheck = 1430; 101 | TargetAttributes = { 102 | A230251C2A6D8DAC00184207 = { 103 | CreatedOnToolsVersion = 14.3; 104 | }; 105 | }; 106 | }; 107 | buildConfigurationList = A23025182A6D8DAC00184207 /* Build configuration list for PBXProject "SwiftVectorSearch" */; 108 | compatibilityVersion = "Xcode 14.0"; 109 | developmentRegion = en; 110 | hasScannedForEncodings = 0; 111 | knownRegions = ( 112 | en, 113 | Base, 114 | ); 115 | mainGroup = A23025142A6D8DAC00184207; 116 | packageReferences = ( 117 | A230252E2A6D8DED00184207 /* XCRemoteSwiftPackageReference "usearch" */, 118 | ); 119 | productRefGroup = A230251E2A6D8DAC00184207 /* Products */; 120 | projectDirPath = ""; 121 | projectRoot = ""; 122 | targets = ( 123 | A230251C2A6D8DAC00184207 /* SwiftVectorSearch */, 124 | ); 125 | }; 126 | /* End PBXProject section */ 127 | 128 | /* Begin PBXResourcesBuildPhase section */ 129 | A230251B2A6D8DAC00184207 /* Resources */ = { 130 | isa = PBXResourcesBuildPhase; 131 | buildActionMask = 2147483647; 132 | files = ( 133 | A23025282A6D8DAD00184207 /* Preview Assets.xcassets in Resources */, 134 | A23025252A6D8DAD00184207 /* Assets.xcassets in Resources */, 135 | ); 136 | runOnlyForDeploymentPostprocessing = 0; 137 | }; 138 | /* End PBXResourcesBuildPhase section */ 139 | 140 | /* Begin PBXSourcesBuildPhase section */ 141 | A23025192A6D8DAC00184207 /* Sources */ = { 142 | isa = PBXSourcesBuildPhase; 143 | buildActionMask = 2147483647; 144 | files = ( 145 | A23025212A6D8DAC00184207 /* SwiftVectorSearchApp.swift in Sources */, 146 | ); 147 | runOnlyForDeploymentPostprocessing = 0; 148 | }; 149 | /* End PBXSourcesBuildPhase section */ 150 | 151 | /* Begin XCBuildConfiguration section */ 152 | A23025292A6D8DAD00184207 /* Debug */ = { 153 | isa = XCBuildConfiguration; 154 | buildSettings = { 155 | ALWAYS_SEARCH_USER_PATHS = NO; 156 | CLANG_ANALYZER_NONNULL = YES; 157 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 158 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 159 | CLANG_ENABLE_MODULES = YES; 160 | CLANG_ENABLE_OBJC_ARC = YES; 161 | CLANG_ENABLE_OBJC_WEAK = YES; 162 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 163 | CLANG_WARN_BOOL_CONVERSION = YES; 164 | CLANG_WARN_COMMA = YES; 165 | CLANG_WARN_CONSTANT_CONVERSION = YES; 166 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 167 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 168 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 169 | CLANG_WARN_EMPTY_BODY = YES; 170 | CLANG_WARN_ENUM_CONVERSION = YES; 171 | CLANG_WARN_INFINITE_RECURSION = YES; 172 | CLANG_WARN_INT_CONVERSION = YES; 173 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 174 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 175 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 176 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 177 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 178 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 179 | CLANG_WARN_STRICT_PROTOTYPES = YES; 180 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 181 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 182 | CLANG_WARN_UNREACHABLE_CODE = YES; 183 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 184 | COPY_PHASE_STRIP = NO; 185 | DEBUG_INFORMATION_FORMAT = dwarf; 186 | ENABLE_STRICT_OBJC_MSGSEND = YES; 187 | ENABLE_TESTABILITY = YES; 188 | GCC_C_LANGUAGE_STANDARD = gnu11; 189 | GCC_DYNAMIC_NO_PIC = NO; 190 | GCC_NO_COMMON_BLOCKS = YES; 191 | GCC_OPTIMIZATION_LEVEL = 0; 192 | GCC_PREPROCESSOR_DEFINITIONS = ( 193 | "DEBUG=1", 194 | "$(inherited)", 195 | ); 196 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 197 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 198 | GCC_WARN_UNDECLARED_SELECTOR = YES; 199 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 200 | GCC_WARN_UNUSED_FUNCTION = YES; 201 | GCC_WARN_UNUSED_VARIABLE = YES; 202 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 203 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 204 | MTL_FAST_MATH = YES; 205 | ONLY_ACTIVE_ARCH = YES; 206 | SDKROOT = iphoneos; 207 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 208 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 209 | }; 210 | name = Debug; 211 | }; 212 | A230252A2A6D8DAD00184207 /* Release */ = { 213 | isa = XCBuildConfiguration; 214 | buildSettings = { 215 | ALWAYS_SEARCH_USER_PATHS = NO; 216 | CLANG_ANALYZER_NONNULL = YES; 217 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 218 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 219 | CLANG_ENABLE_MODULES = YES; 220 | CLANG_ENABLE_OBJC_ARC = YES; 221 | CLANG_ENABLE_OBJC_WEAK = YES; 222 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 223 | CLANG_WARN_BOOL_CONVERSION = YES; 224 | CLANG_WARN_COMMA = YES; 225 | CLANG_WARN_CONSTANT_CONVERSION = YES; 226 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 227 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 228 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 229 | CLANG_WARN_EMPTY_BODY = YES; 230 | CLANG_WARN_ENUM_CONVERSION = YES; 231 | CLANG_WARN_INFINITE_RECURSION = YES; 232 | CLANG_WARN_INT_CONVERSION = YES; 233 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 234 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 235 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 236 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 237 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 238 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 239 | CLANG_WARN_STRICT_PROTOTYPES = YES; 240 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 241 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 242 | CLANG_WARN_UNREACHABLE_CODE = YES; 243 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 244 | COPY_PHASE_STRIP = NO; 245 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 246 | ENABLE_NS_ASSERTIONS = NO; 247 | ENABLE_STRICT_OBJC_MSGSEND = YES; 248 | GCC_C_LANGUAGE_STANDARD = gnu11; 249 | GCC_NO_COMMON_BLOCKS = YES; 250 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 251 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 252 | GCC_WARN_UNDECLARED_SELECTOR = YES; 253 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 254 | GCC_WARN_UNUSED_FUNCTION = YES; 255 | GCC_WARN_UNUSED_VARIABLE = YES; 256 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 257 | MTL_ENABLE_DEBUG_INFO = NO; 258 | MTL_FAST_MATH = YES; 259 | SDKROOT = iphoneos; 260 | SWIFT_COMPILATION_MODE = wholemodule; 261 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 262 | VALIDATE_PRODUCT = YES; 263 | }; 264 | name = Release; 265 | }; 266 | A230252C2A6D8DAD00184207 /* Debug */ = { 267 | isa = XCBuildConfiguration; 268 | buildSettings = { 269 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 270 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 271 | CODE_SIGN_STYLE = Automatic; 272 | CURRENT_PROJECT_VERSION = 1; 273 | DEVELOPMENT_ASSET_PATHS = "\"SwiftVectorSearch/Preview Content\""; 274 | DEVELOPMENT_TEAM = 4J62EE2G74; 275 | ENABLE_PREVIEWS = YES; 276 | GENERATE_INFOPLIST_FILE = YES; 277 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 278 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 279 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 280 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 281 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 282 | LD_RUNPATH_SEARCH_PATHS = ( 283 | "$(inherited)", 284 | "@executable_path/Frameworks", 285 | ); 286 | MARKETING_VERSION = 1.0; 287 | PRODUCT_BUNDLE_IDENTIFIER = com.ashvardanian.SwiftVectorSearch; 288 | PRODUCT_NAME = "$(TARGET_NAME)"; 289 | SWIFT_EMIT_LOC_STRINGS = YES; 290 | SWIFT_VERSION = 5.0; 291 | TARGETED_DEVICE_FAMILY = "1,2"; 292 | }; 293 | name = Debug; 294 | }; 295 | A230252D2A6D8DAD00184207 /* Release */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 299 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 300 | CODE_SIGN_STYLE = Automatic; 301 | CURRENT_PROJECT_VERSION = 1; 302 | DEVELOPMENT_ASSET_PATHS = "\"SwiftVectorSearch/Preview Content\""; 303 | DEVELOPMENT_TEAM = 4J62EE2G74; 304 | ENABLE_PREVIEWS = YES; 305 | GENERATE_INFOPLIST_FILE = YES; 306 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 307 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 308 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 309 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 310 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 311 | LD_RUNPATH_SEARCH_PATHS = ( 312 | "$(inherited)", 313 | "@executable_path/Frameworks", 314 | ); 315 | MARKETING_VERSION = 1.0; 316 | PRODUCT_BUNDLE_IDENTIFIER = com.ashvardanian.SwiftVectorSearch; 317 | PRODUCT_NAME = "$(TARGET_NAME)"; 318 | SWIFT_EMIT_LOC_STRINGS = YES; 319 | SWIFT_VERSION = 5.0; 320 | TARGETED_DEVICE_FAMILY = "1,2"; 321 | }; 322 | name = Release; 323 | }; 324 | /* End XCBuildConfiguration section */ 325 | 326 | /* Begin XCConfigurationList section */ 327 | A23025182A6D8DAC00184207 /* Build configuration list for PBXProject "SwiftVectorSearch" */ = { 328 | isa = XCConfigurationList; 329 | buildConfigurations = ( 330 | A23025292A6D8DAD00184207 /* Debug */, 331 | A230252A2A6D8DAD00184207 /* Release */, 332 | ); 333 | defaultConfigurationIsVisible = 0; 334 | defaultConfigurationName = Release; 335 | }; 336 | A230252B2A6D8DAD00184207 /* Build configuration list for PBXNativeTarget "SwiftVectorSearch" */ = { 337 | isa = XCConfigurationList; 338 | buildConfigurations = ( 339 | A230252C2A6D8DAD00184207 /* Debug */, 340 | A230252D2A6D8DAD00184207 /* Release */, 341 | ); 342 | defaultConfigurationIsVisible = 0; 343 | defaultConfigurationName = Release; 344 | }; 345 | /* End XCConfigurationList section */ 346 | 347 | /* Begin XCRemoteSwiftPackageReference section */ 348 | A230252E2A6D8DED00184207 /* XCRemoteSwiftPackageReference "usearch" */ = { 349 | isa = XCRemoteSwiftPackageReference; 350 | repositoryURL = "https://github.com/unum-cloud/usearch"; 351 | requirement = { 352 | branch = main; 353 | kind = branch; 354 | }; 355 | }; 356 | /* End XCRemoteSwiftPackageReference section */ 357 | 358 | /* Begin XCSwiftPackageProductDependency section */ 359 | A230252F2A6D8DED00184207 /* USearch */ = { 360 | isa = XCSwiftPackageProductDependency; 361 | package = A230252E2A6D8DED00184207 /* XCRemoteSwiftPackageReference "usearch" */; 362 | productName = USearch; 363 | }; 364 | /* End XCSwiftPackageProductDependency section */ 365 | }; 366 | rootObject = A23025152A6D8DAC00184207 /* Project object */; 367 | } 368 | -------------------------------------------------------------------------------- /SwiftVectorSearch.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftVectorSearch.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftVectorSearch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "ce0ab2afe11b0b4d307b08cee634a785136562aefbb651840d04dabefdfed439", 3 | "pins" : [ 4 | { 5 | "identity" : "usearch", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/unum-cloud/usearch", 8 | "state" : { 9 | "branch" : "main", 10 | "revision" : "e7140e55967a4198f2bbd34987fa7597ebafa649" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /SwiftVectorSearch.xcodeproj/xcuserdata/av.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SwiftVectorSearch.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SwiftVectorSearch/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SwiftVectorSearch/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SwiftVectorSearch/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftVectorSearch/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftVectorSearch/SwiftVectorSearchApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftVectorSearchApp.swift 3 | // SwiftVectorSearch 4 | // 5 | // Created by Ashot Vardanian on 7/23/23. 6 | // 7 | 8 | import SwiftUI 9 | import MapKit 10 | 11 | import USearch 12 | 13 | class SearchManager: ObservableObject { 14 | @Published var annotations: [MKPointAnnotation] = [] 15 | @Published var title: String = "" 16 | var index: USearchIndex 17 | var points: [[Float32]] 18 | 19 | init() { 20 | index = USearchIndex.make(metric: .IP, dimensions: 2, connectivity: 16, quantization: .F32) 21 | points = [] 22 | setupSearchIndex() 23 | } 24 | 25 | func setupSearchIndex() { 26 | index = USearchIndex.make(metric: .haversine, dimensions: 2, connectivity: 16, quantization: .F32) 27 | let _ = index.reserve(1000) 28 | 29 | // Center point in Yerevan, Armenia 30 | points = (0..<1000).map { _ in 31 | [40.18306093751397 + Float32.random(in: -0.5...0.5), 44.52643090940268 + Float32.random(in: -0.5...0.5)] 32 | } 33 | points.enumerated().forEach { i, coordinates in 34 | let _ = index.add(key: USearchKey(i), vector: coordinates) 35 | } 36 | } 37 | 38 | func recomputeSearchResults(center: CLLocationCoordinate2D, visibleMapRect: MKMapRect) { 39 | let neMapPoint = MKMapPoint(x: visibleMapRect.maxX, y: visibleMapRect.minY) 40 | let swMapPoint = MKMapPoint(x: visibleMapRect.minX, y: visibleMapRect.maxY) 41 | let neCoord = neMapPoint.coordinate 42 | let swCoord = swMapPoint.coordinate 43 | let latRange = min(swCoord.latitude, neCoord.latitude)...max(swCoord.latitude, neCoord.latitude) 44 | let lonRange = min(swCoord.longitude, neCoord.longitude)...max(swCoord.longitude, neCoord.longitude) 45 | 46 | let results = index.search(vector: [center.latitude, center.longitude], count: 5) 47 | annotations = results.0.filter({ (key: USearchKey) in 48 | let coordinates = points[Int(key)] 49 | return latRange.contains(CLLocationDegrees(coordinates[0])) && lonRange.contains(CLLocationDegrees(coordinates[1])) 50 | }).map { (key: USearchKey) in 51 | let coordinates = points[Int(key)] 52 | let annotation = MKPointAnnotation() 53 | annotation.title = String(key) 54 | annotation.coordinate = CLLocationCoordinate2D(latitude: CLLocationDegrees(coordinates[0]), longitude: CLLocationDegrees(coordinates[1])) 55 | return annotation 56 | } 57 | title = "Showing \(annotations.count) / \(index.length) points" 58 | } 59 | } 60 | 61 | struct MapView: UIViewRepresentable { 62 | var annotations: [MKPointAnnotation] 63 | var onRegionChange: (MKMapRect) -> Void 64 | @Binding var centerCoordinate: CLLocationCoordinate2D 65 | 66 | func makeUIView(context: Context) -> MKMapView { 67 | let mapView = MKMapView() 68 | mapView.delegate = context.coordinator 69 | return mapView 70 | } 71 | 72 | func updateUIView(_ uiView: MKMapView, context: Context) { 73 | uiView.removeAnnotations(uiView.annotations) 74 | uiView.addAnnotations(annotations) 75 | } 76 | 77 | func makeCoordinator() -> Coordinator { 78 | Coordinator(self) 79 | } 80 | 81 | class Coordinator: NSObject, MKMapViewDelegate { 82 | var parent: MapView 83 | 84 | init(_ parent: MapView) { 85 | self.parent = parent 86 | } 87 | 88 | func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { 89 | parent.centerCoordinate = mapView.centerCoordinate 90 | parent.onRegionChange(mapView.visibleMapRect) 91 | } 92 | } 93 | } 94 | 95 | 96 | @main 97 | struct SwiftVectorSearchApp: App { 98 | @StateObject var searchManager = SearchManager() 99 | @State private var centerCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 40.18306093751397, longitude: 44.52643090940268) 100 | 101 | var body: some Scene { 102 | WindowGroup { 103 | NavigationView { 104 | MapView(annotations: searchManager.annotations, onRegionChange: { visibleMapRect in 105 | DispatchQueue.main.async { 106 | searchManager.recomputeSearchResults(center: centerCoordinate, visibleMapRect: visibleMapRect) 107 | } 108 | }, centerCoordinate: $centerCoordinate) 109 | .edgesIgnoringSafeArea(.all) 110 | .navigationBarTitleDisplayMode(.inline) 111 | .navigationBarItems(leading: 112 | Text(searchManager.title) 113 | .font(.headline) // Make it bold 114 | .frame(maxWidth: .infinity, alignment: .center) // Align horizontally 115 | ) 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /dataset.zip: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:daca60fc5be6ac1b2dd7e4ab2664449f87c584314f730e8899c9c88adcc6683a 3 | size 427077442 4 | -------------------------------------------------------------------------------- /images.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Dataset Preparation\n", 8 | "\n", 9 | "## Download" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "!wget \"https://huggingface.co/datasets/unum-cloud/ann-unsplash-25k/resolve/main/images.zip?download=true\" -O images.zip\n", 19 | "!unzip images.zip\n", 20 | "!ls images | wc -l" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "!unzip images.zip\n", 30 | "!ls images | wc -l" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "!wget \"https://huggingface.co/datasets/unum-cloud/ann-unsplash-25k/resolve/main/images.csv?download=true\" -O images.csv" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "!wget \"https://huggingface.co/datasets/unum-cloud/ann-unsplash-25k/resolve/main/images.txt?download=true\" -O images.txt" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "!pip install uform pandas pillow" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "## Embed" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "images_base64 = open(\"images.txt\", \"r\").readlines()\n", 74 | "images_base64" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "from io import BytesIO\n", 84 | "from base64 import b64decode\n", 85 | "from PIL import Image\n", 86 | "import re\n", 87 | "\n", 88 | "def data_to_image(data_uri: str) -> Image:\n", 89 | " \"\"\"Convert a base64-encoded data URI to a Pillow Image.\"\"\"\n", 90 | " # Find the base64 string portion by removing the prefix using regex\n", 91 | " base64_str = re.search(r'base64,(.*)', data_uri).group(1)\n", 92 | " \n", 93 | " # Decode the base64 string\n", 94 | " image_data = b64decode(base64_str)\n", 95 | " \n", 96 | " # Read the image data into a BytesIO buffer and open it with PIL\n", 97 | " image = Image.open(BytesIO(image_data))\n", 98 | " \n", 99 | " return image\n", 100 | "\n", 101 | "data_to_image(images_base64[0])" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": null, 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "import uform\n", 111 | "model, processor = uform.get_model('unum-cloud/uform-vl-english-small')" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "import os\n", 121 | "from tqdm import tqdm\n", 122 | "\n", 123 | "vectors = []\n", 124 | "batch_size = 32\n", 125 | "\n", 126 | "for i in tqdm(range(0, len(images_base64), batch_size), desc=\"Vectorizing images\"):\n", 127 | " batch = images_base64[i:i+batch_size]\n", 128 | " images = [data_to_image(image_base64) for image_base64 in batch]\n", 129 | " image_data = processor.preprocess_image(images)\n", 130 | " image_embeddings = model.encode_image(image_data)\n", 131 | " vectors.extend(image_embeddings)\n", 132 | "\n", 133 | "len(vectors)" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": 2, 139 | "metadata": {}, 140 | "outputs": [], 141 | "source": [ 142 | "from usearch.index import Index\n", 143 | "from usearch.io import load_matrix\n", 144 | "\n", 145 | "vectors = load_matrix(\"images.uform3-image-text-english-small.fbin\")" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": 3, 151 | "metadata": {}, 152 | "outputs": [ 153 | { 154 | "data": { 155 | "text/plain": [ 156 | "usearch.Index\n", 157 | "- config\n", 158 | "-- data type: ScalarKind.F16\n", 159 | "-- dimensions: 256\n", 160 | "-- metric: MetricKind.Cos\n", 161 | "-- multi: False\n", 162 | "-- connectivity: 16\n", 163 | "-- expansion on addition :128 candidates\n", 164 | "-- expansion on search: 64 candidates\n", 165 | "- binary\n", 166 | "-- uses OpenMP: 0\n", 167 | "-- uses SimSIMD: 1\n", 168 | "-- supports half-precision: 1\n", 169 | "-- uses hardware acceleration: neon\n", 170 | "- state\n", 171 | "-- size: 24,292 vectors\n", 172 | "-- memory usage: 38,012,352 bytes\n", 173 | "-- max level: 3\n", 174 | "--- 0. 24,292 nodes\n", 175 | "--- 1. 1,607 nodes\n", 176 | "--- 2. 131 nodes\n", 177 | "--- 3. 24 nodes" 178 | ] 179 | }, 180 | "execution_count": 3, 181 | "metadata": {}, 182 | "output_type": "execute_result" 183 | } 184 | ], 185 | "source": [ 186 | "index = Index(ndim=vectors.shape[1])\n", 187 | "index.add(None, vectors)\n", 188 | "index" 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": null, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "index.save(\"images.uform3-image-text-english-small.usearch\")" 198 | ] 199 | } 200 | ], 201 | "metadata": { 202 | "kernelspec": { 203 | "display_name": "base", 204 | "language": "python", 205 | "name": "python3" 206 | }, 207 | "language_info": { 208 | "codemirror_mode": { 209 | "name": "ipython", 210 | "version": 3 211 | }, 212 | "file_extension": ".py", 213 | "mimetype": "text/x-python", 214 | "name": "python", 215 | "nbconvert_exporter": "python", 216 | "pygments_lexer": "ipython3", 217 | "version": "3.10.11" 218 | } 219 | }, 220 | "nbformat": 4, 221 | "nbformat_minor": 2 222 | } 223 | --------------------------------------------------------------------------------