├── .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 | 
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 |
13 |
14 | |
15 |
16 |
17 | |
18 |
19 |
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 |
--------------------------------------------------------------------------------