Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2026 AI & Machine Learning

WWDC26 · 16 min · AI & Machine Learning

LLM search using Core Spotlight

Level up basic search into a retrieval-augmented system using SpotlightSearchTool and LanguageModelSession. Explore Core Spotlight integration, delegate-based hydration patterns, and how metadata quality impacts your search results. Learn how to use custom PipelineStages for tasks like sentiment analysis. Discover best practices for indexing and building flexible, context-rich search experiences in your app.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

  • 0:00 — Introduction
  • 1:41 — Grounding answers with Spotlight tool-calling
  • 4:00 — Configure and add SpotlightSearchTool
  • 6:44 — Displaying results and partial replies
  • 6:46 — Provide full items with an index delegate
  • 8:12 — Customizing with guidance profiles
  • 11:02 — Reference resolution with a contact resolver
  • 11:24 — Custom pipeline stages
  • 12:47 — Evaluating response quality
  • 15:53 — Next steps

Code shown on screen · 12 snippets

Ask the model with a Foundation Models session swift · at 0:59 ↗
let response = try await session.respond(to: "What are some nice hikes near water?")
Set up SpotlightSearchTool swift · at 4:20 ↗
// Set up SpotlightSearchTool
  import CoreSpotlight
  import FoundationModels

  // In one line, the tool is ready to search your app's Core Spotlight index
  let tool = SpotlightSearchTool()

  // Or provide a custom configuration — e.g. search file paths in your app's sandbox
  let fileTool = SpotlightSearchTool(
      configuration: .init(
          sources: [
              .files
          ]
      )
  )
Add SpotlightSearchTool to a session swift · at 4:50 ↗
// Add SpotlightSearchTool to a session
  import CoreSpotlight
  import FoundationModels
  
  let tool = SpotlightSearchTool()

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

  let response = try await session.respond(to: "What hikes have I gone on?")
Implement an index delegate swift · at 6:24 ↗
// Implement an index delegate
  import CoreSpotlight

  class IndexDelegate: NSObject, CSSearchableIndexDelegate {

      // Called when the index requests searchable items for the provided identifiers
      func searchableItems(forIdentifiers identifiers: [String]) async -> [CSSearchableItem] {
          let entries = await mystore.fetchEntries(ids: identifiers)
          return entries.map { makeSearchableItem(from: $0) }
      }
  }
Track the query token for refresh swift · at 7:37 ↗
// Track the query token for refresh
  import CoreSpotlight
  import FoundationModels

  let tool = SpotlightSearchTool()

  for await reply in tool.searchResults {
  
      if reply.queryToken != currentToken {
          // New query — start a new display section
          currentToken = reply.queryToken
      }

      switch reply.content {
      case .items(let searchItems):
      }
  }
Set a dynamic guidance profile swift · at 8:42 ↗
// Set a dynamic guidance profile
  import CoreSpotlight
  import FoundationModels

  let profile = SpotlightSearchTool.GuidanceProfile(
      textMatch: true,
      dates: true,
      people: false,
      attributes: [.title, .altitude, .completionDate]
  )

  let tool = SpotlightSearchTool(
      configuration: .init(
          guide: .init(level: .dynamic(profile))
      )
  )

  // On-device models have smaller context — prefer focused guidance
  let focusedTool = SpotlightSearchTool(
      configuration: .init(
          guide: .init(level: .focused(.items))
      )
  )
Implement a ContactResolver swift · at 9:32 ↗
// Implement a ContactResolver
  import CoreSpotlight
  import FoundationModels

  struct MyContactResolver: ContactResolver {
  
      func userIdentity() -> ResolvedContact {
          // Pull from whatever identity source your app has —
          // account profile, Contacts framework, sign-in session, etc.
          var contact = ResolvedContact(displayName: "Jane Doe")
          contact.emailAddresses = ["[email protected]", "[email protected]"]
          contact.names = ["Jane", "JD"]
          return contact
      }
  }
  
  tool.contactResolver = MyContactResolver()
Define a custom stage swift · at 11:34 ↗
// Define a custom stage
  import CoreSpotlight
  import FoundationModels

  @Generable
  struct HappinessStage: CustomStage {
      static var name = "happiness"
      static var description = "Scores hike by how happy the author was"
      static var inputTypes: [SearchPipelineDataType] = [.items]
      static var outputTypes: [SearchPipelineDataType] = [.scoredItems]

      @Guide(description: "Minimum happiness score (0.0-1.0) to include in results")
      var threshold: Double?

      func execute(on input: SearchPipelineData) async throws -> SearchPipelineData {
          return SearchPipelineData(payload: .scoredItems(sorted))
      }
  }

  // Register the stage by adding it to the tool's configuration
  let tool = SpotlightSearchTool(configuration: .init(
      customStages: [.happinessBoost(threshold: 0.5)])
  )
Handle a reply data types swift · at 12:10 ↗
// Handle a reply data types
  import CoreSpotlight
  import FoundationModels

  for await reply in tool.searchResults {

      let label = reply.label
      case .items(let searchItems):
      case .scoredItems(let scored):
      case .groupedItems(let groups):
      case .count(let count):
      case .table(let table):
      case .statistic(let statistic):
      case .text(let text):
          continue
      }   
  }
Define an evaluation dataset with ModelSampleProtocol swift · at 13:47 ↗
// Evaluations
  import Evaluations
  
  struct TrailRequest: ModelSampleProtocol {
  
      typealias ExpectedValue = String                    // sample response
      typealias Expectation   = TrajectoryExpectation     
      
      var input:  ModelSampleInput
      var output: ModelSampleOutput<String, TrajectoryExpectation>
      
      var expectedIdentifiers: [String]
  }
Define the trajectory expectation swift · at 15:06 ↗
// Evaluations
  import Evaluations
  
  TrajectoryExpectation(
      unordered: [
          ToolExpectation("searchSpotlight", arguments: [.keyOnly(argumentName: "query")])
      ]   
  )
Run the evaluation test — swift · at 15:17 ↗
@Test("Trail search evaluation meets quality thresholds")
  func trailSearchEval() async throws {
  
      let items = try Self.loadItems()
      let samples = try Self.loadSamples()
      
      try await Self.indexDelegate.indexSearchableItems(items)
      let tool = Self.makeSearchTool()
      
      let evaluation = TrailSearchEvaluation(
          tool: tool,
          dataset: ArrayLoader(samples: samples)
      )   
      
      let result = try await evaluation.run()
      let coverageMean = result.aggregateValue(.mean(of: Metric("ResultCoverage")))
      #expect(coverageMean >= 0.5, "Result coverage should be at least 50% across queries")
  }

Resources