Our previous environment had just one web application and no existing search scopes beyond the default ones. With our upgrade we wanted to (finally) take advantage of search scopes to help filter the result sets and make searches more relevant. In order to make the creation of scopes scriptable I needed three new commands: gl-createsearchscope, gl-updatesearchscope, and gl-addsearchrule. I thought about creating commands to support editing and deleting but as I don’t currently have the need for that I decided against it (with the exception of the gl-updatesearchscope command which I needed to be able to assign my shared search scope to groups on the various web applications). For some reason I was expecting this to be more difficult than it was but after digging into it I found it to be rather easy. The commands I created are detailed below.

gl-createsearchscope

The code to work with search scopes is really straight forward. You obtain a Microsoft.Office.Server.Search.Administration.Scopes object which is effectively your scope manager object. From this you use the AllScopes property (which is a ScopeCollection object) and call the Create method passing in appropriate parameters. Once you’ve got your scope created you can add it to relavent groups by getting the ScopeDisplayGroup object via the GetDisplayGroup() method of the Scopes object. Note that the scope can be owned by a site collection or the SSP. If a null value is passed into the Create method for the owningSiteUrl parameter then the scope will be owned by the SSP (it will be a shared scope available to all site collections belonging to the SSP which is determined by the passed in url parameter which loads the appropriate SPSite object). The core code is shown below:

 1public override int Run(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
 2{
 3    output = string.Empty;
 4
 5    InitParameters(keyValues);
 6
 7    string url = Params["url"].Value.TrimEnd('/');
 8    string name = Params["name"].Value;
 9    string description = Params["description"].Value;
10    string searchPage = null;
11    if (Params["searchpage"].UserTypedIn)
12        searchPage = Params["searchpage"].Value;
13    bool sspIsOwner = Params["sspisowner"].UserTypedIn;
14
15    using (SPSite site = new SPSite(url))
16    {
17        Scopes scopeManager = new Scopes(SearchContext.GetContext(site));
18
19        // Create the scope
20        Scope scope = scopeManager.AllScopes.Create(name, description, (sspIsOwner ? null : new Uri(site.Url)), true,
21        searchPage, ScopeCompilationType.AlwaysCompile);
22
23        // If the user passed in any groups then add the scope to those groups.
24        if (Params["groups"].UserTypedIn)
25        {
26            foreach (string g in Params["groups"].Value.Split(','))
27            {
28                ScopeDisplayGroup group;
29                try
30                {
31                    group = scopeManager.GetDisplayGroup(new Uri(site.Url), g.Trim());
32                }
33                catch (Exception)
34                {
35                    group = null;
36                }
37                if (group == null)
38                {
39                    scope.Delete(); // We don't want the scope created if there was an error with the groups so undo what we've done.
40                    throw new SPException(string.Format("Display group '{0}' not found.", g));
41                }
42                group.Add(scope);
43                group.Update();
44            }
45        }
46    }
47
48    return 1;
49}

The syntax of the command can be seen below:

C:\>stsadm -help gl-createsearchscope

stsadm -o gl-createsearchscope

Sets the search scope for a given site collection.

Parameters:
        -url <site collection url>
        -name <scope name>
        [-description <scope description>]
        [-groups <display groups (comma separate multiple groups)>]
        [-searchpage <specific search results page to send users to for results when they search in this scope>]
        [-sspisowner]

Here’s an example of how to create a shared search scope (owned by the SSP):

stsadm –o gl-createsearchscope -url "http://sspadmin/ssp/admin" -name "Search Scope 1" -description "A really helpful search scope." -groups "search dropdown, advanced search" -sspisowner

Note that the group assignments will not show up on other web applications – you must use the updatesearchscope command to associate the scope with groups on each web application of interest.

gl-updatesearchscope

This code is almost identical to that of the gl-createsearchscope command – the main difference is that I’m updating individual properties rather than calling the Create method and I have to clear out existing groups before adding the newly assigned groups:

 1public override int Run(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
 2{
 3    output = string.Empty;
 4
 5    InitParameters(keyValues);
 6
 7    string url = Params["url"].Value.TrimEnd('/');
 8    string name = Params["name"].Value;
 9    string description = Params["description"].Value + string.Empty;
10    string searchPage = null;
11    if (Params["searchpage"].UserTypedIn)
12        searchPage = Params["searchpage"].Value;
13
14    using (SPSite site = new SPSite(url))
15    using (SPWeb web = site.RootWeb)
16    {
17        if (!web.CurrentUser.IsSiteAdmin)
18            throw new UnauthorizedAccessException();
19
20        Scopes scopeManager = new Scopes(SearchContext.GetContext(site));
21
22        Scope scope;
23        try
24        {
25            scope = scopeManager.GetScope(new Uri(site.Url), name);
26        }
27        catch (ScopeNotFoundException)
28        {
29            scope = scopeManager.GetScope(null, name);
30        }
31        if (Params["description"].UserTypedIn)
32            scope.Description = description;
33        if (Params["searchpage"].UserTypedIn)
34            scope.AlternateResultsPage = searchPage;
35
36        scope.Update();
37
38        // If the user passed in any groups then add the scope to those groups.
39        if (Params["groups"].UserTypedIn)
40        {
41            // Clear out any group settings.
42            foreach (ScopeDisplayGroup g in scopeManager.AllDisplayGroups)
43            {
44                if (g.Contains(scope))
45                {
46                    g.Remove(scope);
47                    g.Update();
48                }
49            }
50
51            // Add back the specified groups.
52            foreach (string g in Params["groups"].Value.Split(','))
53            {
54                ScopeDisplayGroup group;
55                try
56                {
57                    group = scopeManager.GetDisplayGroup(new Uri(site.Url), g.Trim());
58                }
59                catch (Exception)
60                {
61                    group = null;
62                }
63                if (group == null)
64                {
65                    scope.Delete(); // We don't want the scope created if there was an error with the groups so undo what we've done.
66                    throw new SPException(string.Format("Display group '{0}' not found.", g));
67                }
68                group.Add(scope);
69                group.Update();
70            }
71        }
72    }
73
74    return 1;
75}

The syntax of the command can be seen below:

C:\>stsadm -help gl-updatesearchscope

stsadm -o gl-updatesearchscope

Updates the specified search scope for a given site collection.

Parameters:
        -url <site collection url>
        -name <scope name>
        [-description <scope description>]
        [-groups <display groups (comma separate multiple groups)>]
        [-searchpage <specific search results page to send users to for results when they search in this scope>]

Here’s an example of how to update a web application to assign the shared scope created above to appropriate groups:

stsadm –o gl-updatesearchscope -url "http://intranet" -name "Search Scope 1" -groups "search dropdown, advanced search"

gl-addsearchrule

Once you have a search scope created you can now add rules to it. This command is slightly more complex due to the different types of rules that can be created. In general there are four types: AllContent, ContentSource, PropertyQuery, and WebAddress. The ContentSource is typically only used with shared scopes (you can create a ContentSource rule on a scope that is not shared using this tool but you cannot do it via the browser – I’m honestly not sure if the rule will work correctly though). To manage the rules of a scope you simply grab the Rules property of the Scope object and call the appropriate method (there’s one for each type of rule except for ContentSource which is effectively just a PropertyQuery rule that uses the ContentSource managed property):

 1public override int Run(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
 2{
 3    output = string.Empty;
 4
 5    InitParameters(keyValues);
 6
 7    string url = Params["url"].Value.TrimEnd('/');
 8    string scopeName = Params["scope"].Value;
 9    PageType type = (PageType)Enum.Parse(typeof(PageType), Params["type"].Value, true);
10
11    ScopeRuleFilterBehavior behavior = ScopeRuleFilterBehavior.Include;
12    if (Params["behavior"].UserTypedIn)
13        behavior = (ScopeRuleFilterBehavior)Enum.Parse(typeof(ScopeRuleFilterBehavior), Params["behavior"].Value, true);
14
15
16    using (SPSite site = new SPSite(url))
17    {
18        SearchContext context = SearchContext.GetContext(site);
19        Scopes scopeManager = new Scopes(context);
20        Scope scope = scopeManager.GetScope(new Uri(site.Url), scopeName);
21        Schema schema = new Schema(context);
22
23        switch(type)
24        {
25            case PageType.AllContent:
26                scope.Rules.CreateAllContentRule();
27                break;
28            case PageType.ContentSource:
29                scope.Rules.CreatePropertyQueryRule(behavior, schema.AllManagedProperties["ContentSource"], Params["propertyvalue"].Value);
30                break;
31            case PageType.PropertyQuery:
32                ManagedProperty prop;
33                try
34                {
35                    prop = schema.AllManagedProperties[Params["property"].Value];
36                }
37                catch (KeyNotFoundException)
38                {
39                    throw new SPException(
40                    string.Format("Property '{0}' was not found.", Params["property"].Value));
41                }
42                scope.Rules.CreatePropertyQueryRule(behavior, prop, Params["propertyvalue"].Value);
43
44                break;
45            case PageType.WebAddress:
46                UrlScopeRuleType webType =
47                    (UrlScopeRuleType) Enum.Parse(typeof (UrlScopeRuleType), Params["webtype"].Value, true);
48                scope.Rules.CreateUrlRule(behavior, webType, Params["webvalue"].Value);
49                break;
50        }
51    }
52
53    return 1;
54}

The syntax of the command can be seen below:

C:\>stsadm -help gl-addsearchrule

stsadm -o gl-addsearchrule

Adds a search scope rule to the specified scope for a given site collection.

Parameters:
        -url <site collection url>
        -scope <scope name>
        -behavior <include | require | exclude>
        -type <webaddress | propertyquery | contentsource | allcontent>
        [-webtype <folder | hostname | domain>]
        [-webvalue <value associated with the specified web type>]
        [-property <managed property name>]
        [-propertyvalue <value associated with the specified property or content source>]

Here’s an example of how to add a rule to the scope created above which will prevent content from the HR site collection from being returned in the results:

stsadm –o gl-addsearchrule -url "http://intranet" -scope "Search Scope 1" -behavior exclude -type webaddress -webtype folder -webvalue "http://intranet/hr"