All Articles
CategoryAI
Reading Time
8 min read
Published
2026-10-15
Word Count
1,628words

Grab a coffee — this one is a deep dive!

SwiftIntelligence 2.0: on-device search, answers and redaction for an app's own text

Summary

Why SwiftIntelligence 2.0 is one small Swift target: hybrid BM25 and vector search, cited on-device answers, and redaction that survives look-alike text.

  • Apps hold text that users want to search or ask about: notes, help articles, saved messages.
  • The 1.x layout of many modules hid how little of the code did real work.
  • Add https://github.com/muhittincamdali/SwiftIntelligence.git with Swift Package Manager from 2.0.0, then add documents and search them.
  • The README roadmap lists two next steps: PDFs and images (PDFKit and Vision text recognition) as document sources, and a Core ML sentence-encoder embedder for stronger semantic search.
SwiftIntelligence 2.0: on-device search, answers and redaction for an app's own text

Table of Contents


The problem

Apps hold text that users want to search or ask about: notes, help articles, saved messages.

Apple ships most of the pieces to do this on the device. NaturalLanguage

tokenizes and embeds text, Accelerate multiplies matrices quickly, and from iOS 26 and macOS 26

Foundation Models gives access to an on-device language model. What is missing is the layer

that joins them: take a document, split it into passages, remove personal data, embed it, index

it, search it, hand the best passages to a model with instructions to cite them, and save the

whole thing between launches.

SwiftIntelligence 2.0 is that layer, and only that layer. It is one library target with no

dependencies, for iOS 18 and macOS 15. It does not train models, process images or audio, or

act as a server-side vector database.

The narrow scope comes from a rewrite. The repository opened in August 2025 as a broad

"AI/ML framework". By version 1.2.2 it had grown to 16 modules and, in the words of the

2.0.0 changelog, "about 50,000 lines, most of which returned simulated results (random

confidences, fixed sentences, provider stubs)". In June 2026 a cleanup commit

(`32a1ca7`) removed

release scripts and unused modules but also added a path dependency on a sibling folder,

../SwiftAI, so the manifest no longer resolved on any other machine. On 14 October 2026

the first commit of the rewrite

(`0e2dcbd`) deleted the

old modules, and PR #9 merged

the result as v2.0.0 on 15 October. No 1.x API was kept, which is why it is a major version.

Design decisions and alternatives

One target, no dependencies. The 1.x layout of many modules hid how little of the code did

real work. For about 2,400 lines of source, separate library products (core, search, privacy,

generation) would add maintenance without helping a user who just wants to add one package.

The code can still be split later if it grows.

KnowledgeBase is an actor. In the Swift 6 language mode an actor is the shortest way to

keep the mutable indexes free of data races. Actors are reentrant, though: another call can run

while one is suspended at an await. So add first computes and validates every embedding of

a batch, the only part that awaits, and then stores everything with no suspension point. The

source says it plainly: "No suspension point from here on: concurrent calls cannot interleave

their changes." An embedding error therefore leaves the knowledge base unchanged instead of

half-indexed. search follows the same rule: its one await is embedding the query.

A lock-guarded class would not be checked by the compiler, and @MainActor would run

searches on the UI thread.

Hybrid search with reciprocal rank fusion. Every passage is indexed twice: by its words with

BM25 in KeywordIndex, and as a vector in VectorIndex. Vectors match passages that use other

words; BM25 matches exact codes such as "E-4012" that a small embedding model can miss. Their

scores are on different scales, so a weighted sum would need an arbitrary normalization. RankFusion uses only positions instead: an

item's fused score is the sum of weight / (k + rank) over the lists, with

defaultSmoothing = 60.0, "the usual smoothing constant from Cormack, Clarke and Büttcher

(2009)". Ties keep the order in which items first appear, so results are deterministic.

Exact search, no approximate index. VectorIndex stores unit-length rows in one flat array,

so cosine similarity is a dot product, and a search is a single vDSP_mmul call over all rows.

The target is one user's or one app's documents, which the README puts at "up to tens of

thousands of passages". At that size exact search is simple and has no recall loss, while an

approximate index such as HNSW would add memory and complexity. The cost is that search time grows linearly with the number of vectors.

A hashing embedder as the default. HashingEmbedder hashes words and three-character pieces

into 512 dimensions. It needs no model, works in every language and on the iOS simulator, and

is deterministic in tests. It uses its own 64-bit FNV-1a, because, as the source comment

explains, "Swift's Hasher is seeded per process, so it cannot be used for vectors that are

saved and compared later." SentenceEmbedder and ContextualEmbedder wrap Apple's embeddings,

and any type conforming to TextEmbedder can be plugged in. The embedder's identifier is saved

with the data, and loading with a different one throws KnowledgeBaseError.embedderMismatch

rather than comparing vectors that are not comparable.

Grounded answers that may refuse. GroundedPrompt numbers the passages and tells the model

to answer only from them and to cite them as [1]. The passage budget defaults to 6,000

characters. When nothing matches, answer(_:) throws AnswerError.noRelevantPassages

without calling the model at all. The answer code sits behind canImport(FoundationModels) and

availability checks, so the package still builds for iOS 18 and macOS 15.

A real bug: redaction that leaked look-alike text

PersonalDataRedactor replaces email addresses, card numbers, IBANs and other personal data

with placeholders such as [EMAIL_1]. Its first version ran its patterns on the raw text, and

the tests used neatly formatted samples, so they passed. But a redactor's job is precisely the

text that is not neatly formatted.

Commit `3e96601`

records what a systematic try found: "Against the previous code, 10 of 12 sample values leaked

completely": card numbers grouped with en dashes, dots or a line break, a card after another

digit group ("Order 12 4111 1111 1111 1111"), full-width and accented email addresses, IBANs

followed directly by a code, and IPv4 addresses in full-width digits. Overlapping matches were

resolved by dropping the shorter one, which left its non-overlapping part visible.

The fix changed where matching happens. Patterns now run on a folded copy of the text

(FoldedText): format characters removed, NFKC compatibility forms, combining marks stripped,

every dash turned into "-" and every space into " ". A map of the changed characters takes each

match back to the original text. Card candidates are runs of digit groups, and inside each run

the longest window that passes the Luhn check wins. IBANs are cut to their country's length and

checked with mod 97. Overlapping matches are merged. Later commits closed more variants of the

same class, and the RedactionBypassTests suite keeps all of them, including look-alikes that

must stay visible, such as numbers that fail the checksum.

A second fix shows a related lesson. VectorMath.normalized divided a vector

by its largest value with vDSP, and Accelerate flushes subnormal numbers to zero, so a finite

vector like [1e-39, 0, 0, 0] became [inf, nan, nan, nan]. Adding and saving succeeded,

but loading rejected the saved vectors, and the knowledge base could no longer be opened.

Commit `aabec82`

scales such vectors by 2^64 in plain Swift first, and a test now adds, saves, loads and

searches a document whose embedding is subnormal. The lesson: check an invariant when the

data is written and again when it is read, and test the round trip.

Usage

Add https://github.com/muhittincamdali/SwiftIntelligence.git with Swift Package Manager

from 2.0.0, then add documents and search them. This sample is copied from the README, where

it is compiled by the example app and run by the package tests through ReadmeSamples.swift:

swift
1let knowledge = KnowledgeBase()
2try await knowledge.add("You can return any item within 30 days of delivery for a full refund.", id: "returns")
3try await knowledge.add("Orders ship within two business days; delivery takes three to five days.", id: "shipping")
4try await knowledge.add("Every laptop comes with a two-year warranty against defects.", id: "warranty")
5 
6let results = try await knowledge.search("How do I return an item for a refund?")
7print(results.first?.documentID ?? "nothing found") // returns
8print(results.first?.text ?? "") // You can return any item within 30 days …

Redaction is reversible, so a model can work with placeholders and the app can put the values

back into its answer:

swift
1let redactor = PersonalDataRedactor()
2let redacted = redactor.redact("Mail jane@example.com or pay with 4111 1111 1111 1111.")
3print(redacted.text) // "Mail [EMAIL_1] or pay with [CARD_1]."
4 
5// After a model has answered from the redacted text, put the values back.
6let reply = redacted.restoringOriginals(in: "I wrote to [EMAIL_1].")
7print(reply) // "I wrote to jane@example.com."

At tag v2.0.0, swift test on macOS runs 156 tests in 22 suites; the README reports 155 on an

iPhone simulator, where tests that need missing NaturalLanguage models are skipped with a reason.

Limits

  • Scale. Indexes are exact and in memory. They suit one user's or one app's documents, up to

tens of thousands of passages. The 2.0.0 release publishes no benchmark.

  • Original text is not saved. A saved knowledge base holds the redacted passages and their

vectors. Switching embedders means adding the documents again.

  • Redaction is a safety net, not a guarantee. Data that is spelled out ("jane at example dot

com"), split by ordinary words, or invalid by its checksum is not found, and name detection is

statistical.

  • Semantic quality depends on Apple's models. They are small general models, and the iOS

simulator does not have them. The README recommends keeping hybrid mode unless your own tests

show semantic search alone is better.

  • Answers need iOS 26 or macOS 26 on a device with Apple Intelligence turned on, and a

model can follow instructions written inside documents; SECURITY.md lists this as a known

limit.

  • Saved files are not encrypted by the package. SECURITY.md suggests

.completeFileProtection when saving.

The README roadmap lists two next steps: PDFs and images (PDFKit and Vision text recognition)

as document sources, and a Core ML sentence-encoder embedder for stronger semantic search.

Source code: github.com/muhittincamdali/SwiftIntelligence

Tags

#swift#on-device#search#foundation-models#privacy#swift-package
Muhittin Çamdalı

Muhittin Çamdalı

Senior Mobile Engineer

Senior Mobile Engineer with 12+ years of experience. I build mobile products with iOS, Flutter and React Native, working full-stack and AI-native.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

Your subscription starts when you open the link in the confirmation email and press “Confirm my subscription”. The newsletter keeps open/click statistics; you can unsubscribe anytime with one click. Privacy

Share