<!--
{
  "availability" : [
    "iOS: 27.0.0 -",
    "Xcode: 27.0.0 -"
  ],
  "documentType" : "article",
  "framework" : "CoreSpotlight",
  "identifier" : "/documentation/CoreSpotlight/searching-indexed-content-with-natural-language",
  "metadataVersion" : "0.1.0",
  "role" : "sampleCode",
  "title" : "Searching indexed content with natural language"
}
-->

# Searching indexed content with natural language

Give a language model access to your app’s Core Spotlight index to enable natural-language queries over searchable content.

## Overview

This sample demonstrates [`SpotlightSearchTool`](/documentation/CoreSpotlight/SpotlightSearchTool), a type that connects a Foundation Models language-model session to your app’s Core Spotlight index. Using `SpotlightSearchTool`, the language model can search, filter, and reason about your indexed content, turning a metadata-based index into a conversational search experience.

![A person asks “What hikes are by the ocean?” in the sample app’s search field. Matching trail cards, including Crystal Cove State Park and Lands End Trail, appear above a streamed summary of nearby coastal hikes.](images/com.apple.corespotlight/spotlightsearchtool-hero@2x.png)

The app indexes a collection of hiking trail entries as [`CSSearchableItem`](/documentation/CoreSpotlight/CSSearchableItem) objects, then lets people ask natural-language questions like “Which trails in California have water features?” The language model uses the tool to query the index and streams a response alongside the matching trail results.

> Note: This sample code project is associated with WWDC26 session [246: LLM search using Core Spotlight](https://developer.apple.com/wwdc26/246/).

## Configure the sample code project

This sample requires a device that supports Apple Intelligence, running iOS 27 or later.

Before you build and run the sample, turn on Apple Intelligence by opening Settings > Apple Intelligence & Siri.

By default, the sample runs searches on the on-device, so the project builds and runs without additional configuration. For best performance, route searches through Private Cloud Compute (PCC). For additional information, see [Adopt Private Cloud Compute](/documentation/CoreSpotlight/searching-indexed-content-with-natural-language#Adopt-Private-Cloud-Compute).

## Create a search tool for the language model

The sample creates a [`SpotlightSearchTool`](/documentation/CoreSpotlight/SpotlightSearchTool) configured with a Core Spotlight source to let the language model search the indexed content. The `fetchAttributes` parameter specifies which item attributes the tool returns to the model, providing the information it uses to answer questions about trails. The sample includes both built-in attributes and a custom distance attribute that the app indexes for each trail:

```swift
let fetchAttributes: [SearchableItemAttribute] = [
    .title,
    .contentDescription,
    .namedLocation,
    .stateOrProvince,
    .keywords,
    .latitude,
    .longitude,
    .rating,
    .duration,
    .contentCreationDate,
    .completionDate,
    SearchableItemAttribute(rawValue: distanceAttributeKey.keyName)
]

let tool = SpotlightSearchTool(
    configuration: .init(
        sources: [
            .coreSpotlight(
                .init(
                    searchableIndexDelegate: SpotlightIndexer.shared,
                    fetchAttributes: fetchAttributes
                )
            )
        ],
        guide: .focused()
    )
)
```

## Adopt Private Cloud Compute

By default, the sample runs searches on the on-device <doc://com.apple.documentation/documentation/FoundationModels/SystemLanguageModel>, so the project builds and runs without additional configuration. The view model exposes the model it uses as a `serverModel` property:

```swift
let serverModel = SystemLanguageModel()
```

To route searches through Private Cloud Compute (PCC) instead, initialize `serverModel` with <doc://com.apple.documentation/documentation/FoundationModels/PrivateCloudComputeLanguageModel>. When `serverModel` is the PCC model, the search tool uses the [`SpotlightSearchTool.GuidanceLevel.complete`](/documentation/CoreSpotlight/SpotlightSearchTool/GuidanceLevel/complete) guide for richer query construction; on device, it uses [`SpotlightSearchTool.GuidanceLevel.focused(_:)`](/documentation/CoreSpotlight/SpotlightSearchTool/GuidanceLevel/focused(_:)) and provides more explicit search instructions to suit the smaller model. For eligibility and setup, see <doc://com.apple.documentation/documentation/FoundationModels/adding-server-side-intelligence-with-private-cloud-compute>.

## Stream responses from the language model

The sample passes the search tool to a <doc://com.apple.documentation/documentation/FoundationModels/LanguageModelSession> along with system instructions that describe the indexed data. When a person submits a query, the session calls the tool to find matching entries and streams a natural-language response. The sample creates a fresh session and tool for each search so every query starts with fresh context:

```swift
let session = LanguageModelSession(
    model: serverModel,
    tools: [tool],
    instructions: instructions
)

do {
    for try await chunk in session.streamResponse(to: prompt) {
        response = chunk.content
    }
} catch {
    self.error = error.localizedDescription
}
```

## Display search results alongside the response

The search tool provides an asynchronous stream of search replies as the model processes the query. Each reply’s `content` is a discriminated union: matches arrive as [`SpotlightSearchTool.SearchReply.Content.items(_:)`](/documentation/CoreSpotlight/SpotlightSearchTool/SearchReply/Content-swift.enum/items(_:)), [`SpotlightSearchTool.SearchReply.Content.scoredItems(_:)`](/documentation/CoreSpotlight/SpotlightSearchTool/SearchReply/Content-swift.enum/scoredItems(_:)), or [`SpotlightSearchTool.SearchReply.Content.groupedItems(_:)`](/documentation/CoreSpotlight/SpotlightSearchTool/SearchReply/Content-swift.enum/groupedItems(_:)) that provide wrapped [`SearchableItem`](/documentation/CoreSpotlight/SearchableItem) results. Additionally, the model may also return other [`SpotlightSearchTool.SearchReply.Content`](/documentation/CoreSpotlight/SpotlightSearchTool/SearchReply/Content-swift.enum) enumeration values as replies, depending on the query.

The sample listens for results on this stream and updates the UI as items arrive, so trail cards appear before the model finishes generating its text summary. Because the model can issue multiple queries while refining results, the sample deduplicates by `uniqueIdentifier` to avoid showing the same trail twice. The sample unwraps each `SearchableItem` to the underlying `CSSearchableItem` at this boundary, and the rest of the UI works directly with Core Spotlight’s own item type:

```swift
private func listenForSearchResults(from tool: SpotlightSearchTool) -> Task<Void, Never> {
    Task { @MainActor in
        var seen: Set<String> = []
        for await reply in tool.searchResults {
            let items: [CSSearchableItem]
            switch reply.content {
            case .items(let searchItems):
                items = searchItems.map(\.item)
            case .scoredItems(let scored):
                items = scored.map(\.item.item)
            case .groupedItems(let groups):
                items = groups.values.flatMap { $0 }.map(\.item)
            case .count, .table, .statistic, .text:
                continue
            @unknown default:
                continue
            }
            let newItems = items.filter { seen.insert($0.uniqueIdentifier).inserted }
            self.results.append(contentsOf: newItems)
        }
    }
}
```

## Index searchable items with Core Spotlight

The sample loads trail data from a property list at launch and indexes each entry as a [`CSSearchableItem`](/documentation/CoreSpotlight/CSSearchableItem). Each item includes attributes like title, location, keywords, and duration. The indexer uses [`beginBatch()`](/documentation/CoreSpotlight/CSSearchableIndex/beginBatch()) and [`endBatch(withClientState:completionHandler:)`](/documentation/CoreSpotlight/CSSearchableIndex/endBatch(withClientState:completionHandler:)) to group the work into a single transaction, and records client state so it can skip reindexing on subsequent launches:

```swift
func indexAllItems() async {
    let items = createSearchableItems()
    guard !items.isEmpty else { return }

    var isIndexed = true
    let newState = Data(bytes: &isIndexed, count: MemoryLayout.size(ofValue: isIndexed))

    do {
        index.beginBatch()
        try await index.indexSearchableItems(items)
        try await index.endBatch(withClientState: newState)
    } catch {
        print("Batch index failed: \(error.localizedDescription)")
    }
}
```

The indexer conforms to [`CSSearchableIndexDelegate`](/documentation/CoreSpotlight/CSSearchableIndexDelegate) so the system can request full searchable items when needed during hydration, which enriches the generated response.

---

Copyright &copy; 2026 Apple Inc. All rights reserved. | [Terms of Use](https://www.apple.com/legal/internet-services/terms/site.html) | [Privacy Policy](https://www.apple.com/privacy/privacy-policy)