A macOS desktop app (Swift) with a local Rust-powered search service that supports keyword and semantic search, all offline. The Rust crate builds a cdylib exposed over a C FFI for Swift.
Repo layout:
repo-root
├─ app/ (Swift - to be added)
│ ├─ SearchApp.xcodeproj
│ └─ Sources/
├─ searchsvc/ (Rust → C FFI)
│ ├─ Cargo.toml
│ └─ src/
│ └─ lib.rs
├─ models/ (Core ML assets)
└─ scripts/ (helper Bash)
- Xcode 15+
- macOS 14+
- Rust (via
rustup):curl https://sh.rustup.rs -sSf | sh - cbindgen (for header generation):
cargo install cbindgen
Optional for model conversion:
- Python 3.10+
coremltools,torch,onnxruntimeif you plan to convert or validate models locally
- Run tests
./scripts/test.sh- Build the dynamic library and generate a C header
./scripts/build.shOutputs:
- dylib:
searchsvc/target/release/libsearchsvc.dylib - header:
searchsvc/target/ffi/searchsvc.h
- Domain (Entities):
Document,SearchHitinsearchsvc/src/lib.rs - Application (Use cases):
SearchServicewith operations: index text/file/dir and query - Ports: C FFI functions (
searchsvc_init,searchsvc_index_*,searchsvc_query_json) - Adapters:
- Swift adapter calls C FFI
- Core ML adapter computes embeddings in Swift and passes float vectors to Rust
- Infrastructure:
- File IO (indexing file/dir)
- Storage (in-memory for demo; move to files in
~/Library/Application Support/SearchApplater)
CRUD mapping:
- Create/Update:
index_text,index_file,index_directory(re-index to update) - Read:
query - Delete: not implemented yet; can be added by removing doc IDs from the maps
Yes. Place Core ML models in models/ and include in the .app bundle. All inference runs on-device (CPU/GPU/ANE) via Core ML. Recommended starter:
- Sentence embedding:
all-MiniLM-L6-v2converted to Core ML (≈100 MB). Produces 384–768-dim embeddings. - Alternatively use ONNX + onnxruntime-metal. Bundle the
.onnxinmodels/and linkonnxruntimeif you prefer.
Typical sizes (fit comfortably on Mac laptops):
- Embedding model: ~100 MB (Core ML)
- Inverted index (keyword for ~1M docs): ~1–2 GB
- Flat vector index (768-d fp32): ~3 GB per million vectors
Keep everything offline in the user’s Application Support directory, e.g. ~/Library/Application Support/SearchApp/.
- Create the Xcode project under
app/(macOS App, Swift). Add a target calledSearchApp. - Add a build phase to copy the Rust dylib and header into the app:
- Run Script Phase after build:
- Input:
$(SRCROOT)/../searchsvc/target/release/libsearchsvc.dylib,$(SRCROOT)/../searchsvc/target/ffi/searchsvc.h - Copy the dylib to
$(TARGET_BUILD_DIR)/$(EXECUTABLE_FOLDER_PATH)/and the header to$(DERIVED_FILE_DIR)/
- Input:
- Run Script Phase after build:
- Bridging header / module map:
- Add a Swift bridging header and import
searchsvc.h:#include "searchsvc.h"
- In Build Settings, set
SWIFT_OBJC_BRIDGING_HEADERto the path of your bridging header.
- Add a Swift bridging header and import
- Linker search path:
- Add
$(PROJECT_DIR)/../searchsvc/target/releasetoLibrary Search Pathsand setRunpath Search Pathsto include@executable_path/.
- Add
- Swift wrapper (pseudo-interface):
- Create a Swift class that wraps the FFI calls: initialize handle, index files (e.g.,
~/.aws/credentials), and callsearchsvc_query_json.
- Create a Swift class that wraps the FFI calls: initialize handle, index files (e.g.,
- Embeddings in Swift:
- Load the Core ML embedding model from the app bundle; for each document and for queries, compute a float vector and pass to Rust.
~/.aws/credentialsand~/.aws/config~/.ssh/config~/Documents,~/Desktop,~/Library/Mobile Documents/com~apple~CloudDocs- Workspace folders you choose
You can index a directory via the FFI using searchsvc_index_file in a loop, or add a Swift-side crawler that reads files and calls searchsvc_index_text.
- Rust unit tests:
./scripts/test.sh - Manual functional test:
- Build Rust:
./scripts/build.sh - Start the app in Xcode.
- On first launch, index
~/.aws/credentials. - Search for
aws_access_key_idanddefaultand verify the file and snippets show up. - Test semantic search by querying synonyms (e.g., "AWS key" → should retrieve the credentials file if embeddings are indexed).
- Build Rust:
- Persist indices to disk (JSON or SQLite for keyword; FAISS/
sqlite-vecfor vectors) - Incremental file watching (FSEvents) and re-index on change
- Add delete/update APIs on the Rust side
- Add a small HTTP port for alternative integration (keep FFI for in-process fast calls)
- If Xcode cannot find the dylib at runtime, verify the Runpath Search Paths include
@executable_path/and the dylib is copied into the app bundle. - If cbindgen is missing:
cargo install cbindgenand rerun./scripts/build.sh. - If model fails to load, ensure the
.mlmodelcis inside the app bundle’s resources.