Core Spotlight - Foundation Models - OS 27

Local RAG with Core Spotlight

OS 27 lets an app turn its existing Core Spotlight index into retrieval context for Foundation Models. SpotlightSearchTool gives a LanguageModelSession a local path to app-specific notes, documents, tasks, messages, and records without first building a server-side vector database.

June 18, 2026 8 minute read WWDC26 session 246
Trail Search app with a natural-language search query
Apple's WWDC26 Core Spotlight demo shows a natural-language query over app-indexed trail notes.

Highlights

Index once

App records become CSSearchableItems with stable identifiers and meaningful metadata.

Retrieve locally

SpotlightSearchTool lets a model search app-indexed content through the tool-calling path.

Hydrate results

An index delegate can return complete items when compact indexed text is not enough for model context.

Ground the UI

Read searchResults, group replies by query token, and show source records beside the answer.

The Architecture

RAG here is not a new database. It is a retrieval path through data your app already knows how to describe. The canonical store remains your database, document package, synced model, or file system. Core Spotlight holds a searchable projection. The language model sees only the records selected for the current prompt.

App records
  -> CSSearchableItem + CSSearchableItemAttributeSet
  -> CSSearchableIndex
  -> SpotlightSearchTool
  -> LanguageModelSession
  -> answer + source records in your UI

Apple's session describes that same trajectory: the model decides that it needs the Spotlight tool, generates a search, Spotlight runs it, and the model reasons over the returned result description before producing the final response.

SpotlightSearchTool requires OS 27 SDK

The older indexing side is broader. CSSearchableIndex, CSSearchableItem, and CSSearchableItemAttributeSet already exist in Core Spotlight. The new OS 27 piece is making that index available to Foundation Models through SpotlightSearchTool.

Index App Content

Start with one durable, user-recognizable thing: a note, task, trip, recipe, document, thread, or ticket. Give it a stable identifier, an optional domain identifier for cleanup, an accurate content type, and enough text or metadata for retrieval to find it later.

import CoreSpotlight
import UniformTypeIdentifiers

func makeSearchableItem(from note: Note) -> CSSearchableItem {
    let attributes = CSSearchableItemAttributeSet(contentType: .text)
    attributes.title = note.title
    attributes.contentDescription = note.summary
    attributes.textContent = note.searchableText
    attributes.keywords = note.tags
    attributes.contentModificationDate = note.updatedAt

    return CSSearchableItem(
        uniqueIdentifier: "note:\(note.accountID):\(note.id)",
        domainIdentifier: "account.\(note.accountID).notes",
        attributeSet: attributes
    )
}

let index = CSSearchableIndex(name: "Notes")
let item = makeSearchableItem(from: note)

index.indexSearchableItems([item]) { error in
    if let error {
        print("Core Spotlight indexing failed: \(error)")
    }
}

Calling indexSearchableItems with the same identifier updates the item. When the source record is deleted, remove the matching searchable item. When a whole account, workspace, or project becomes inaccessible, delete the relevant domain rather than leaving stale search results behind.

index.deleteSearchableItems(
    withIdentifiers: ["note:\(accountID):\(noteID)"]
) { error in
    if let error {
        print("Spotlight deletion failed: \(error)")
    }
}

index.deleteSearchableItems(
    withDomainIdentifiers: ["account.\(accountID).notes"]
) { error in
    if let error {
        print("Spotlight domain deletion failed: \(error)")
    }
}

Prefer system-defined attributes where they match your content. A title should be a real user-facing label. A description should be a readable summary. Keywords should be tags, aliases, categories, or domain terms, not generated synonym soup.

Attach Spotlight to the Session

The simplest adoption path is intentionally small: create a SpotlightSearchTool, pass it into a LanguageModelSession, and ask the model a question that needs app content.

import CoreSpotlight
import FoundationModels

let tool = SpotlightSearchTool()

let session = LanguageModelSession(
    tools: [tool]
)

let response = try await session.respond(
    to: "Which project notes mention a launch risk?"
)

Custom configuration is for when the default path is too broad. OS 27 adds search sources such as CoreSpotlightSource for app-indexed items and FileSource for indexed files or directories visible to Spotlight. It also adds guidance, contact resolution, and custom pipeline stages for more specialized retrieval.

let fileTool = SpotlightSearchTool(
    configuration: .init(
        sources: [
            .files
        ]
    )
)

Instructions still matter, but they are not authorization. Tell the model to answer from retrieved records and say when sources are missing. Keep account checks, permission checks, and destructive operations in deterministic app code.

Hydrate Full Items

A record can be searchable without being readable. Apple notes that some indexed metadata, including text and HTML, can be stored in a compact representation that remains searchable but cannot be recovered in a form a language model can read. The OS 27 index delegate path lets the app return complete searchable items by identifier while the tool is running.

import CoreSpotlight

final class IndexDelegate: NSObject, CSSearchableIndexDelegate {
    private let store: NoteStore

    init(store: NoteStore) {
        self.store = store
    }

    func searchableItems(
        forIdentifiers identifiers: [String]
    ) async -> [CSSearchableItem] {
        let notes = await store.fetch(ids: identifiers)
        return notes.map { makeSearchableItem(from: $0) }
    }
}

let index = CSSearchableIndex(name: "Notes")
let delegate = IndexDelegate(store: store)
index.indexDelegate = delegate

Hydration should recheck the current account, permissions, deletion state, and record version. It is a read path, not a place to mark records as read, trigger workflows, or trust a stale index entry as proof that access is still valid.

Return Sources, Not Just Answers

respond(to:) gives the generated answer. searchResults gives the app the retrieval stream. Use both: answer text for the assistant view, source records for trust and navigation.

for await reply in tool.searchResults {
    let token = reply.queryToken

    // App code: group source UI by query token.
    sourcePresenter.startOrUpdateSection(for: token)

    // App code: inspect reply content, load current records
    // from your canonical store, and show source cards.
    await sourcePresenter.consume(reply)
}

The query token matters because one user prompt may produce multiple Spotlight searches before the final response. Keep each retrieval batch separate enough that the interface can explain what was found and let the user open the original record.

A source card should use canonical app data, not stale indexed text. Show the current title, project or account, relevant date, short excerpt, and an action that opens the original item.

Keep the Index Fresh

Every change to search-visible data needs an index consequence. New records are indexed. Edited search fields are reindexed under the same identifier. Deleted records are removed. Logout and workspace switching remove inaccessible domains.

App event Index action
Record created Add a searchable item
Title, text, date, or tag changed Reindex the same identifier
Record deleted Delete the searchable item
Account removed Delete that account's domains
Projection format changed Rebuild affected items

For larger catalogs, use batches and client state so interrupted indexing can resume from a known checkpoint. Treat failed deletions as work to retry, not as harmless warnings.

Privacy Boundaries

Core Spotlight retrieval is local. Apple's Core Spotlight guidance describes app donations as private, local indexes that other apps cannot inspect. That is the retrieval boundary, not automatically the generation boundary.

A LanguageModelSession can be backed by different model providers. If the active model is on device, retrieved context stays on device for generation. If the active model uses Private Cloud Compute or another server-backed provider, the retrieved context follows that model path. Write the product copy with that distinction intact.

Do not index content just because the API permits it. Index records the user would expect to search and expect an assistant to use. Avoid secrets, raw logs, hidden moderation signals, deleted records, and content from accounts the current user cannot access.

Guidance, People, and Pipelines

Once the basic path works, OS 27 gives the tool more shape. Guidance profiles can narrow the search capabilities exposed to the model. A contact resolver can help resolve first-person references such as "I" and "me" when searchable metadata includes people. Custom pipeline stages can score or transform result sets for app-specific tasks.

Keep these as measured additions. If a notes app only needs textual item lookup, focused guidance is easier to reason about than exposing every search capability. If exact counts or destructive actions are involved, let the model retrieve and explain candidate records while deterministic app code calculates, validates, and commits.

Fallback for OS 26

Fallback for OS 26

Keep indexing app content with Core Spotlight. Use your normal app search or Core Spotlight search UI for retrieval. If you already have Foundation Models available on an older target, a custom Tool can query your own database or search layer and return a small grounded summary, but it is not the same as SpotlightSearchTool.

Shipping Checklist

Index durable records, not random chunks.

Every source should map back to something the user can recognize and open.

Use stable, account-aware identifiers.

Identifiers are routing keys. Keep them stable and free of secret payloads.

Hydrate with current permissions.

Recheck access before returning full items or showing source cards.

Handle empty retrieval.

If no source supports an answer, make that state visible instead of showing a confident guess.

Test stale and adversarial data.

Include deleted records, account switches, prompt-like document text, and similar record names in evaluation fixtures.

Sources