oreilly.comSafari Books Online.Conferences.


AddThis Social Bookmark Button

Programming with Spotlight
Pages: 1, 2, 3, 4

Query the Spotlight Server

So far, we've displayed the Spotlight search window and examined a specific file's metadata. While interesting and useful, these features still leave plenty to be sought. Querying the Spotlight server helps to fill this void and is one of the ways Spotlight really struts its stuff. In our example application, we'll use Spotlight to query the entire file system for mail messages the same way we could in the Spotlight search window. Take a moment to review the docs on NSMetadataQuery and NSPredicate if you haven't already; these are the cornerstones. Everything else is from the standard Model View Controller repertoire.

Let's update your controller class again. Changes are in bold.

For "Controller.h"

//  Controller.h
//  SpotlightExamples

#import <Cocoa/Cocoa.h>

@interface Controller : NSObject {
    IBOutlet NSButton* openSearchWindowButton;    
    IBOutlet NSTextView* metadataInfoView;
    IBOutlet NSButton* displayMetadataButton;

    IBOutlet NSButton* startSearchButton;
    IBOutlet NSButton* stopSearchButton;
    IBOutlet NSTextField* numHitsField;
    NSMetadataQuery* q;
    NSTimer* t;

- (IBAction)openSearchWindowAction:(id)sender;

- (IBAction)displayMetadataAction:(id)sender;

- (id)init;
- (void)awakeFromNib;
- (void)dealloc;
- (IBAction)startSearchAction:(id)sender;
- (IBAction)stopSearchAction:(id)sender;
- (void)stopSearching;
- (void)updateResults:(NSTimer*)timer;


For "Controller.m"

//  Controller.m
//  SpotlightExamples

#import "Controller.h"

@implementation Controller
- (IBAction)openSearchWindowAction:(id)sender
    OSStatus resultCode=noErr;
    //Replace "Search Text" with user input
        HISearchWindowShow((CFStringRef)@"Search Text", kNilOptions);
    if (resultCode != noErr) {
        NSLog(@"Failed to open search window");
        //Could use NSAlert class to display interactive dialog

- (IBAction)displayMetadataAction:(id)sender
    //create a CF-compliant object representing 
    //a file and its metadata using a Carbon level call
    //add in a path to an existing file on your system
    CFStringRef path = CFSTR("/Users/matthew/temp.txt");
    MDItemRef item = MDItemCreate(kCFAllocatorDefault, path);
    //pull out the metadata attribute names
    CFArrayRef attributeNames = MDItemCopyAttributeNames(item);
    //use toll-free bridging to load up an NSArray for convenience
    NSArray* array = (NSArray*)attributeNames;
    NSEnumerator *e = [array objectEnumerator];
    id arrayObject;
    NSMutableString *info = 
        [NSMutableString stringWithCapacity:50];
    CFTypeRef ref;
    while ((arrayObject = [e nextObject]))
        ref = 
        MDItemCopyAttribute(item, (CFStringRef)[arrayObject description]);
        //cast to get an NSObject for convenience
        NSObject* tempObject = (NSObject*)ref;
        [info appendString:[arrayObject description]];
        [info appendString:@" = "];
        [info appendString:[tempObject description]];
        [info appendString:@"\n"];
    //set the info in the text view
    [metadataInfoView insertText:info];

- (id)init
    if (self = [super init])
        //release in dealloc
        q = [[NSMetadataQuery alloc] init];
    return self;

- (void)awakeFromNib
    [q setDelegate: self];
    //remember to unregister for notifications
    [[NSNotificationCenter defaultCenter] 

- (void)dealloc
    [q release];

    [[NSNotificationCenter defaultCenter] 

    [super dealloc];

- (IBAction)startSearchAction:(id)sender
    //whatever query you want. emlx corresponds 
    //to mail messages. you could easily
    //configure search terms from user interaction.
    NSPredicate *p = 
        [NSPredicate predicateWithFormat:@"kMDItemKind == 'emlx'", nil];
    [q setPredicate:p];
    //optionally set search scopes
    //[q setSearchScopes:
    //    [NSArray arrayWithObject:@"/Users/matthew/Library/Mail/"]];
    //start the query and use the run loop 
    //to process the search progress.
    if ([q startQuery])
        t = 
        [NSTimer scheduledTimerWithTimeInterval:0.25 
        //NSRunLoop retains the timer
        [[NSRunLoop currentRunLoop] 
        NSLog(@"Error. Could not start query. Weird.");

- (IBAction)stopSearchAction:(id)sender
    [self stopSearching];

//called via the NSMetadataQueryDidFinishGatheringNotification 
//and/or the stopSearchAction: method
- (void)stopSearching 
    //don't invalidate a timer more than once
    if (!([q isStopped]))
        //NSLog(@"Finito. Num results = %d", [q resultCount]);
        [self updateResults:t];
        [q stopQuery];
        [t invalidate];

- (void)updateResults:(NSTimer*)timer
    NSString *tempString = 
    [NSString stringWithFormat:@"%d", [[timer userInfo] resultCount], nil];
    [numHitsField setStringValue:tempString];
    //NSLog(@"%d", [[timer userInfo] resultCount]);


Like last time, drag and drop your header file onto Interface Builder's palette so that your instantiated controller reflects the changes. On your application's main menu, you'll need to:

  • Add two NSButtons and an NSTextField and connect their outlets
    • Rename one NSButton "Start Search"
    • Rename one NSButton "Stop Search"
    • Add the NSTextField
    • Connect their outlets as you've been previously doing with Ctrl-click drags.
  • Set the actions for the "Start Search" button and "Stop Search" button
    • Connect "Start Search" to startSearchAction:
    • Connect "Stop Search" to stopSearchAction:
  • Do any finishing touches (optional)
    • Group controls together with an NSBox
    • Title the main window

If you decide to group controls using an NSBox, you have to delete your existing controls, drag "fresh" controls from Interface Builder's palette over onto the box, and then re-establish the outlets and connections. This only takes a few moments, and makes things look a lot less chaotic. Although the application you've developed is fairly pedagogical, the concepts and code snippets used are the same that you'd use in more sophisticated circumstances.

Your project can now query the entire filesystem.

You can get the project file for this final portion here.

Command Line Tools

Before creating a Spotlight plugin next time, you might like to know that you don't necessarily have to be a Cocoa programmer to benefit from Spotlight. Apple provides several very useful command line tools that you can use in shell scripts to query and manipulate metadata. Here's a few you might find handy:

  • mdls: lists the metadata attributes for the specified file
  • mdfind: finds files matching a given query
  • mdimport: imports file hierarchies into the metadata datastore
  • mdutil: manages the metadata stores used by Spotlight

Since this is a Cocoa-oriented tutorial, we won't work through command-line examples. You'll have no problems getting acquainted by using the man pages or Apple's documentation. These command-line tools are very useful for debugging an importer (as we'll see next time) or for use in your Perl, Bash, or other scripting routines that can benefit from the metadata awareness.

On a final note about command line tools, realize that you aren't constrained to the ones Apple provides. You can create your own custom tools using Spotlight's Carbon-level API available to you. In fact, the Cocoa-level API we've primarily been using is simply a wrapper around these Carbon-level calls. The concepts are exactly the same, and you might even enjoy the simplicity gained from the absence of human interaction (every once in a while).

Next Time

For next time, you'll want to skim the documentation on Introduction to Spotlight Importers and ponder how in the world we'll get Spotlight to be aware of notes in Stickies. Until then.

Matthew Russell is a computer scientist from middle Tennessee; and serves Digital Reasoning Systems as the Director of Advanced Technology. Hacking and writing are two activities essential to his renaissance man regimen.

Return to the Mac DevCenter