Provisioning Search on SharePoint 2013 Foundation Using PowerShell
There was recently a twitter conversation between @cacallahan, @toddklindt, and @brianlala discussing provisioning Search on SharePoint Foundation and whether it was possible or not and somewhere during the conversation it was suggested that I might know how to do this (sorry guys for not responding immediately) – unfortunately I hadn’t actually done any work with SharePoint 2013 Foundation yet and so had not yet tried and thus didn’t know the answer (I knew there were issues and suspected a workaround was possible but I didn’t have a server built to test anything). Well, last night and today I managed to have some free time so I figured I’d take a look at the problem to see if my guess about a workaround was correct.
Before I get to the results of my discovery let’s first look at what the blocking issue is. It’s actually quite simple – the product team, for various reasons, have decided that for SharePoint 2013 Foundation you can only have one Search Service Application and you shouldn’t be able to modify the topology of the Service Application; this means that when you provision Search using the Farm Configuration Wizard it will create a default topology for you in which all roles are on a single server. So, to enforce these rules they chose to make it so that the PowerShell cmdlets would not allow you to provision the service application or run any method or cmdlet that would otherwise allow you to modify an existing topology (so you can’t change the topology created by the wizard). I totally get the reasoning for the restriction – if you need enterprise topology type structures then pony up the money and get off the free stuff. That said, I think they took the lazy way out by simply blocking the cmdlets when they could have easily put in other restrictions that would have achieved their goals while still allowing users to use PowerShell to provision the environment.
If you’re curious as to what happens when you try to provision the service using PowerShell on SharePoint Foundation 2013 here’s a screenshot which shows the error that is thrown:

This error is thrown by a simple piece of code in the InternalValidate() method of the cmdlet which checks to make sure you are on Standard or Enterprise before allowing the cmdlet to execute (and any other cmdlets or methods that would otherwise affect the topology likewise perform this check).
To solve the problem I decided to start from the perspective of code run via the browser and drill down to see what I could find. So using Reflector I located the class and associated methods that are called by the Farm Configuration Wizard; this quickly led me to the public Microsoft.Office.Server.Search.Administration.SearchService.CreateApplication() static methods. So I did a quick test calling one of these methods and I was happy to find that the Search Service Application created perfectly – though there was one minor problem: the topology was empty. At first glance I figured this wouldn’t be an issue – I could simply clone the topology and add my components – unfortunately this is where I learned that they applied the SKU check to methods and cmdlets that would allow you to manipulate the topology. (On a side note, using these methods for Standard or Enterprise is potentially a great alternative to the New-SPEnterpriseSearchServiceApplication cmdlet as it lets you specify the names of databases that you can’t specify when using the cmdlet and because it creates an initially empty topology there’s less cleanup and manipulation of the cloned topology (assuming you don’t want to use what’s created) and it provisions slightly faster because it does less). So at this point I figured I’d hit the real road block – I could create the service application but it was useless as I couldn’t manipulate it.
This left me with only one option – to use reflection to call the internal method that the Farm Configuration Wizard calls to provision the service application. Now, before I get to the code that demonstrates how to do this I need to share a word of caution – using reflection to call internal methods is totally not supported. So what does this mean? Will Microsoft no longer support your Farm? Well, my understanding (and folks in the know please correct me if I’m in the wrong) is that Microsoft will continue to support you and that you will simply have to remove unsupported code before they will help you troubleshoot issues. Well, in this case it’s a one-time operation so there’s nothing really to remove; I figure the worst case scenario is that they’ll tell you that you need to recreate the service application using the Farm Configuration Wizard and then they’ll help you with your issue. But let’s take the question of supportability out of the equation for a second and look at it from a completely practical standpoint – if you were to look at the code that the Farm Configuration Wizard calls you’d see that, outside of some error checking and data validation and variable initialization, there’s effectively just two lines of code that do the provisioning of the service so I believe that the probability of getting it wrong is pretty low and the fact is search will either work or it won’t so if it doesn’t work then try again or just use the dang wizard. So, with all that said, if you decide to use any of this code you need to weigh the risks yourself and make an informed decision with those risks in mind. Alright, enough of that crap – you want to see the code so let’s get to the code.
To keep the PowerShell itself nice and simple I decide to derive this example from a script that Todd Klindt provides on his blog (the script I use is considerably more complex as it handles the changing of service options like the index folder and the service and crawl accounts, to name a few, and I don’t want the point of this post to be lost in all those details). Just to make sure the full chain of credit is provided I should note that Todd’s script is actually a derivative of what Spence Harbar provides on his blog but I wanted to reference Todd’s post specifically as it’s a bit shorter and more focused on the topic. Okay, background info – check; disclaimer – check; attribution – check – looks like it’s time for some code so here you go:
Start-SPEnterpriseSearchServiceInstance $env:computername
Start-SPEnterpriseSearchQueryAndSiteSettingsServiceInstance $env:computername
#Provide a unique name for the service application
$serviceAppName = "Search Service Application"
#Get the application pools to use (make sure you change the value for your environment)
$svcPool = Get-SPServiceApplicationPool "SharePoint Services App Pool"
$adminPool = Get-SPServiceApplicationPool "SharePoint Services App Pool"
#Get the service from the service instance so we can call a method on it
#Use reflection to provision the default topology just as the wizard would
$bindings = @("InvokeMethod", "NonPublic", "Instance")
$types = @([string], [Type], [Microsoft.SharePoint.Administration.SPIisWebServiceApplicationPool], [Microsoft.SharePoint.Administration.SPIisWebServiceApplicationPool])
$values = @($serviceAppName, [Microsoft.Office.Server.Search.Administration.SearchServiceApplication], [Microsoft.SharePoint.Administration.SPIisWebServiceApplicationPool]$svcPool, [Microsoft.SharePoint.Administration.SPIisWebServiceApplicationPool]$adminPool)
$methodInfo = $searchService.GetType().GetMethod("CreateApplicationWithDefaultTopology", $bindings, $null, $types, $null)
$searchServiceApp = $methodInfo.Invoke($searchService, $values)
#Create the search service application proxy (we get to use the cmdlet for this!)
$searchProxy = New-SPEnterpriseSearchServiceApplicationProxy -Name "$serviceAppName Proxy" -SearchApplication $searchServiceApp
#Provision the search service application
$searchServiceApp.Provision()
Basically there’s two things that need to be done: first we need to use reflection to get the MethodInfo object for the CreateApplicationWithDefaultTopology() method of the Microsoft.Office.Server.Search.Administration.SearchService class and we’ll use this object to invoke the actual method, passing in the parameter types and values (and yes, the cast of the SPIisWebServiceApplicationPool objects is necessary otherwise you’ll get an error about trying to convert PSObjects to SPIisWebServiceApplicationPool types); the next thing we need to do, after the service application is created, is to create the service application proxy and then call the Provision() method on the search service application that we previously created (if you miss this step you’ll get errors about things like the admin component not be started and whatnot).
Once completed you’ll get a fully functional, PowerShell provisioned search service application. If you navigate to the search administration page you should see something that looks just like this (just like if you used the wizard):
So there you have it – it is indeed possible to provision the service using PowerShell – I’ll let you determine whether you should or not
Happy PowerShelling!
-Gary
Starting the SharePoint 2010 Foundation Search Service using PowerShell
It's been a while since my last real SharePoint 2010 scripting post but we're getting close to RTM so I figured I need to buckle down and play some catch up and get some long overdue posts published. So, continuing my series of posts on scripting the various services and service applications within SharePoint 2010 I decided that I would share something that I know a lot of people have been struggling with recently - scripting the SharePoint Foundation Search Service.
This one threw me for a bit of a loop because all the other services and service applications can be configured almost exclusively using PowerShell cmdlets - this one though has to be configured almost exclusively using the object model. We basically have four cmdlets available to help with the configuration and unfortunately they're not much help at all:
- Get-SPSearchService - Returns back an object representing the actual service
- Get-SPSearchServiceInstance - Returns an object representing a service configuration for the service
- Set-SPSearchService - Updates a few select properties associated with the service
- Set-SPSearchServiceInstance - Updates the ProxyType for the service
The main failing with these cmdlets is that you can't set the services process identity, the database name and server or failover server, and you can't trigger the provisioning of the service instances which is required for the service to be considered fully "started". All of these things I can do through Central Admin but there's no way to do it using any provided cmdlets - so how do we solve the problem? By getting our hands dirty and writing a boat load of code against the object model.
So let's get started. As before we'll use an XML file to drive the setup process:
<Services>
<FoundationSearchService Enable="true"
AddStartAddressForNonNTZone="false"
MaxBackupDuration="2880"
PerformanceLevel="PartlyReduced"
DatabaseServer="SPSQL1"
DatabaseName="SharePoint_Search_Help"
FailoverDatabaseServer="">
<SvcAccount Name="sp2010\spsearch" />
<CrawlAccount Name="sp2010\spcrawl" />
<Servers>
<Server Name="sp2010svr" ProxyType="Default" />
</Servers>
</FoundationSearchService>
</Services>
As you can see the configuration file is pretty simple. We define two accounts that we'll use, one for the process identity of the service and the other for the crawl account. There's a few simple attributes for the database and some miscellaneous configurations and a list of all the servers in which the service should be started on.
Okay, let's start digging into the actual script. The first thing I do is load the XML file to a variable, $svcConfig, which I use throughout the function:
1: [xml]$config = Get-Content $settingsFile
2: $svcConfig = $config.Services.FoundationSearchService
Line 1 loads the file into a System.Xml.XmlDocument typed variable and then I grab the <FoundationSearchService /> element and set that to the $svcConfig variable. Next I need to determine if the script should continue on this server by checking the <Servers /> element to see if there's a match for the current machine:
1: #See if we want to start the svc on the current server.
2: $install = (($svcConfig.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
3: if (!$install) {
4: Write-Host "Machine not specified in Servers element, service will not be started on this server."
5: return
6: }
So at this point we know that we're on a target machine so the first thing we want to do is use the Start-SPServiceInstance to start the Foundation Search Service:
1: #Start the service instance
2: $svc = Get-SPServiceInstance | where {$_.TypeName -eq "SharePoint Foundation Search" -and $_.Parent.Name -eq $env:ComputerName}
3: if ($svc -eq $null) {
4: $svc = Get-SPServiceInstance | where {$_.TypeName -eq "SharePoint Foundation Help Search" -and $_.Parent.Name -eq $env:ComputerName}
5: }
6: Start-SPServiceInstance -Identity $svc
The trick with this is that if we're not using SharePoint Foundation then once the service is initially started it renames itself to "SharePoint Foundation Help Search" so I had to put a provision to look for one name or the other to allow this script to be run multiple times and from multiple machines. Now that the service is started lets set a few variables that we'll use throughout the rest of the script:
1: #Get the service and service instance
2: $searchSvc = Get-SPSearchService
3: $searchSvcInstance = Get-SPSearchServiceInstance -Local
4:
5: $dbServer = $svcConfig.DatabaseServer
6: $failoverDbServer = $svcConfig.FailoverDatabaseServer
We'll use the $searchSvc and $searchSvcInstance variables extensively. Note that we'll also need to repeat lines one and two at least a couple of times to avoid update conflicts as a result of timer jobs modifying those objects.
The next step will be to set the process identity for the service. We'll go ahead and also get the crawl account information while we're at it to avoid prompting for passwords in more than one location:
1: #Get the service account details
2: Write-Host "Provide the username and password for the search crawl account..."
3: $crawlAccount = Get-Credential $svcConfig.CrawlAccount.Name
4: Write-Host "Provide the username and password for the search service account..."
5: $searchSvcAccount = Get-Credential $svcConfig.SvcAccount.Name
6:
7: #Get or Create a managed account for the search service account.
8: $searchSvcManagedAccount = (Get-SPManagedAccount -Identity $svcConfig.SvcAccount.Name -ErrorVariable err -ErrorAction SilentlyContinue)
9: if ($err) {
10: $searchSvcManagedAccount = New-SPManagedAccount -Credential $searchSvcAccount
11: }
12:
13: #Set the account details if different than what is current.
14: $processIdentity = $searchSvc.ProcessIdentity
15: if ($processIdentity.ManagedAccount.Username -ne $searchSvcManagedAccount.Username) {
16: $processIdentity = $searchSvc.ProcessIdentity
17: $processIdentity.CurrentIdentityType = "SpecificUser"
18: $processIdentity.ManagedAccount = $searchSvcManagedAccount
19: Write-Host "Updating the service process identity..."
20: $processIdentity.Update()
21: $searchSvc.Update()
22: }
This is where things start to get interesting. I use the Get-Credential cmdlet to return back the credentials of the user to use for the service but once I have that there's no parameter on any cmdlet that will allow me to set the credential so I have to do it using the object model. I use the $searchSvc variable from earlier and edit the object returned by the ProcessIdentity property (after confirming that the value needs to be changed).
Once we have the process set we can go ahead and set the other simple properties on the service - fortunately the cmdlet Set-SPSearchService can actually help us out with this one:
1: #It doesn't hurt if this runs more than once so we don't bother checking before running.
2: Write-Host "Updating the search service properties..."
3: $searchSvc | Set-SPSearchService `
4: -CrawlAccount $crawlAccount.Username `
5: -CrawlPassword $crawlAccount.Password `
6: -AddStartAddressForNonNTZone $svcConfig.AddStartAddressForNonNTZone `
7: -MaxBackupDuration $svcConfig.MaxBackupDuration `
8: -PerformanceLevel $svcConfig.PerformanceLevel `
9: -ErrorVariable err `
10: -ErrorAction SilentlyContinue
11: if ($err) {
12: throw $err
13: }
Alright, that was the easy stuff - now we have to deal with the database. The first step is to see if there's already a database defined for the service and if it matches what we want. This is important as we want to be able to run the script more than once so we don't want to just blindly delete and recreate the database. The first bit of code builds a connection string using the SqlConnectionStringBuilder object (note that in PowerShell you have to use the PSBase property to access the properties on this object) and then compares that to what is currently set. If a match is not found then the existing database is deleted and the search service updated:
1: #Build the connection string to the new database.
2: [System.Data.SqlClient.SqlConnectionStringBuilder]$builder1 = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
3: $builder1.psbase.DataSource = $dbServer
4: $builder1.psbase.InitialCatalog = $svcConfig.DatabaseName
5: $builder1.psbase.IntegratedSecurity = $true
6: Write-Host "Proposed database connection: {$builder1}"
7:
8: [Microsoft.SharePoint.Search.Administration.SPSearchDatabase]$searchDb = $searchSvcInstance.SearchDatabase
9: $dbMatch = $false
10: if ($searchDb -ne $null) {
11: #A database is already set - if it's the one we expect then keep it, otherwise we want to delete it.
12: [System.Data.SqlClient.SqlConnectionStringBuilder]$builder2 = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($searchDb.DatabaseConnectionString)
13: Write-Host "Existing database connection: {$builder2}"
14: if ($builder2.ConnectionString.StartsWith($builder1.ConnectionString, [StringComparison]::OrdinalIgnoreCase)) {
15: $dbMatch = $true
16: }
17: if (!$dbMatch -and $searchDb.DatabaseConnectionString.Equals($builder1.ConnectionString, [StringComparison]::OrdinalIgnoreCase)) {
18: $dbMatch = $true
19: }
20: if (!$dbMatch) {
21: #The database does not match the configuration provided so delete it.
22: Write-Host "The specified database details do not match existing details. Clearing existing."
23: $searchSvcInstance.SearchDatabase = $null
24: $searchSvcInstance.Update()
25: Write-Host "Deleting {$($searchDb.DatabaseConnectionString)}..."
26: $searchDb.Delete()
27: Write-Host "Finished deleting search DB."
28: $searchDb = $null
29: } else {
30: Write-Host "Existing Database details match provided details ($($builder2))"
31: }
32: }
At this point if the $searchDb variable is null then we want to go ahead and create a new search database:
1: #If we don't have a DB go ahead and create one.
2: if ($searchDb -eq $null) {
3: $dbCreated = $false
4: try
5: {
6: Write-Host "Creating new search database {$builder1}..."
7: $searchDb = [Microsoft.SharePoint.Search.Administration.SPSearchDatabase]::Create($builder1)
8: Write-Host "Provisioning new search database..."
9: $searchDb.Provision()
10: Write-Host "Provisioning search database complete."
11: $dbCreated = $true
12:
13: #Re-get the service to avoid update conflicts
14: $searchSvc = Get-SPSearchService
15: $searchSvcInstance = Get-SPSearchServiceInstance -Local
16:
17: Write-Host "Associating new database with search service instance..."
18: $searchSvcInstance.SearchDatabase = $searchDb
19: Write-Host "Updating search service instance..."
20: $searchSvcInstance.Update()
21:
22: #Re-get the service to avoid update conflicts
23: $searchSvc = Get-SPSearchService
24: $searchSvcInstance = Get-SPSearchServiceInstance -Local
25: }
26: catch
27: {
28: if ($searchDb -ne $null -and $dbCreated) {
29: Write-Warning "An error occurred updating the search service instance, deleting search database..."
30: try
31: {
32: #Clean up
33: $searchDb.Delete()
34: }
35: catch
36: {
37: Write-Warning "Unable to delete search database."
38: Write-Error $_
39: }
40: }
41: throw $_
42: }
43: }
I first create a new SPSearchDatabase object by calling the static Create() method and passing in the SqlConnectionStringBuilder object that was previously created. I then call the Provision() method to actually create the database on the SQL server instance. Once it's created we can associate the database with the service by setting the SearchDatabase property on the $searchSvcInstance variable. If an error occurs then I attempt to delete the database from SQL Server if it's not yet associated with the service.
Now that we have our database provisioned we can go ahead and set the failover server:
1: #Set the database failover server
2: if (![string]::IsNullOrEmpty($failoverDbServer)) {
3: if (($searchDb.FailoverServiceInstance -eq $null) -or `
4: ![string]::Equals($searchDb.FailoverServiceInstance.NormalizedDataSource, $failoverDbServer, [StringComparison]::OrdinalIgnoreCase))
5: {
6: try
7: {
8: Write-Host "Adding failover database instance..."
9: $searchSvcInstance.SearchDatabase.AddFailoverServiceInstance($failoverDbServer)
10: Write-Host "Updating search service instance..."
11: $searchSvcInstance.Update()
12: }
13: catch
14: {
15: Write-Warning "Unable to set failover database server. $_"
16: }
17: }
18: }
Most of the logic here is just in determining whether or not to set the failover server. Basically you just call the AddFailoverServiceInstance() method of the SearchDatabase property (SPSearchDatabase) and then update the service instance.
We're almost there - we've set all the properties we can now we need to complete the provisioning process:
1: $status = $searchSvcInstance.Status
2: #Provision the service instance on the current server
3: if ($status -ne [Microsoft.SharePoint.Administration.SPObjectStatus]::Online) {
4: if ([Microsoft.SharePoint.Administration.SPServer]::Local -eq $searchSvcInstance.Server) {
5: try
6: {
7: Write-Host "Provisioning search service instance..."
8: $searchSvcInstance.Provision()
9: }
10: catch
11: {
12: Write-Warning "The call to SPSearchServiceInstance.Provision (server '$($searchSvcInstance.Server.Name)') failed. Setting back to previous status '$status'. $($_.Exception)"
13: if ($status -ne $searchSvcInstance.Status) {
14: try
15: {
16: $searchSvcInstance.Status = $status
17: $searchSvcInstance.Update()
18: }
19: catch
20: {
21: Write-Warning "Failed to restore previous status on the SPSearchServiceInstance (server '$($searchSvcInstance.Server.Name)'). $($_.Exception)"
22: }
23: }
24: throw $_
25: }
26: }
27: }
If the service instance is not currently marked as Online (again, accounting for multiple runs) and the service instance we're working with is for the current machine then we call the Provision() method on the service instance. If an error occurs provisioning the service then I try to set the status back to its previous value.
Only two steps left; First we need to create a timer job to trigger the search service instance to be provisioned on the other servers in the farm:
1: #Re-get the service to avoid update conflicts
2: $searchSvc = Get-SPSearchService
3:
4: #Create the timer job to update the instances for the other servers.
5: foreach ($serviceInstance in $searchSvc.Instances) {
6: if ($serviceInstance -is [Microsoft.SharePoint.Search.Administration.SPSearchServiceInstance] `
7: -and $serviceInstance -ne $searchSvcInstance `
8: -and $serviceInstance.Status -eq [Microsoft.SharePoint.Administration.SPObjectStatus]::Online) {
9: $definition = $serviceInstance.Farm.GetObject("job-service-instance-$($serviceInstance.Id.ToString())", $serviceInstance.Farm.TimerService.Id, [Microsoft.SharePoint.Administration.SPServiceInstanceJobDefinition])
10: if ($definition -ne $null) {
11: Write-Host "A provisioning job for the SPSearchServiceInstance on server '$($serviceInstance.Server.Name)' already exists; skipping."
12: } else {
13: Write-Host "Creating provisioning job for the SPSearchServiceInstance on server '$($serviceInstance.Server.Name)'..."
14: $job = New-Object Microsoft.SharePoint.Administration.SPServiceInstanceJobDefinition($serviceInstance, $true)
15: $job.Update($true)
16: }
17: }
18: }
And finally, we need to set the ProxyType for the service instances so I loop through the <Server /> elements and call the Set-SPSearchServiceInstance cmdlet, providing the ProxyType attribute as defined in the XML:
1: #Set the proxy type for all the service instances.
2: $svcConfig.Servers.Server | ForEach-Object {
3: $server = $_
4: $instance = Get-SPSearchServiceInstance | where {$_.Server.Name -eq $server.Name}
5: if ($instance -ne $null `
6: -and $server.ProxyType.ToLowerInvariant() -ne $instance.ProxyType.ToLowerInvariant) {
7: Write-Host "Setting proxy type for $($instance.Server.Name) to $($server.ProxyType)..."
8: $instance | Set-SPSearchServiceInstance -ProxyType $server.ProxyType
9: }
10: }
Phew - we're done! Let's put it all together now - here's the complete script:
function Start-FoundationSearch([string]$settingsFile = "Configurations.xml") {
[xml]$config = Get-Content $settingsFile
$svcConfig = $config.Services.FoundationSearchService
#See if we want to start the svc on the current server.
$install = (($svcConfig.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
if (!$install) {
Write-Host "Machine not specified in Servers element, service will not be started on this server."
return
}
#Start the service instance
$svc = Get-SPServiceInstance | where {$_.TypeName -eq "SharePoint Foundation Search" -and $_.Parent.Name -eq $env:ComputerName}
if ($svc -eq $null) {
$svc = Get-SPServiceInstance | where {$_.TypeName -eq "SharePoint Foundation Help Search" -and $_.Parent.Name -eq $env:ComputerName}
}
Start-SPServiceInstance -Identity $svc
#Get the service and service instance
$searchSvc = Get-SPSearchService
$searchSvcInstance = Get-SPSearchServiceInstance -Local
$dbServer = $svcConfig.DatabaseServer
$failoverDbServer = $svcConfig.FailoverDatabaseServer
#Get the service account details
Write-Host "Provide the username and password for the search crawl account..."
$crawlAccount = Get-Credential $svcConfig.CrawlAccount.Name
Write-Host "Provide the username and password for the search service account..."
$searchSvcAccount = Get-Credential $svcConfig.SvcAccount.Name
#Get or Create a managed account for the search service account.
$searchSvcManagedAccount = (Get-SPManagedAccount -Identity $svcConfig.SvcAccount.Name -ErrorVariable err -ErrorAction SilentlyContinue)
if ($err) {
$searchSvcManagedAccount = New-SPManagedAccount -Credential $searchSvcAccount
}
#Set the account details if different than what is current.
$processIdentity = $searchSvc.ProcessIdentity
if ($processIdentity.ManagedAccount.Username -ne $searchSvcManagedAccount.Username) {
$processIdentity = $searchSvc.ProcessIdentity
$processIdentity.CurrentIdentityType = "SpecificUser"
$processIdentity.ManagedAccount = $searchSvcManagedAccount
Write-Host "Updating the service process identity..."
$processIdentity.Update()
$searchSvc.Update()
}
#It doesn't hurt if this runs more than once so we don't bother checking before running.
Write-Host "Updating the search service properties..."
$searchSvc | Set-SPSearchService `
-CrawlAccount $crawlAccount.Username `
-CrawlPassword $crawlAccount.Password `
-AddStartAddressForNonNTZone $svcConfig.AddStartAddressForNonNTZone `
-MaxBackupDuration $svcConfig.MaxBackupDuration `
-PerformanceLevel $svcConfig.PerformanceLevel `
-ErrorVariable err `
-ErrorAction SilentlyContinue
if ($err) {
throw $err
}
#Build the connection string to the new database.
[System.Data.SqlClient.SqlConnectionStringBuilder]$builder1 = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
$builder1.psbase.DataSource = $dbServer
$builder1.psbase.InitialCatalog = $svcConfig.DatabaseName
$builder1.psbase.IntegratedSecurity = $true
Write-Host "Proposed database connection: {$builder1}"
[Microsoft.SharePoint.Search.Administration.SPSearchDatabase]$searchDb = $searchSvcInstance.SearchDatabase
$dbMatch = $false
if ($searchDb -ne $null) {
#A database is already set - if it's the one we expect then keep it, otherwise we want to delete it.
[System.Data.SqlClient.SqlConnectionStringBuilder]$builder2 = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($searchDb.DatabaseConnectionString)
Write-Host "Existing database connection: {$builder2}"
if ($builder2.ConnectionString.StartsWith($builder1.ConnectionString, [StringComparison]::OrdinalIgnoreCase)) {
$dbMatch = $true
}
if (!$dbMatch -and $searchDb.DatabaseConnectionString.Equals($builder1.ConnectionString, [StringComparison]::OrdinalIgnoreCase)) {
$dbMatch = $true
}
if (!$dbMatch) {
#The database does not match the configuration provided so delete it.
Write-Host "The specified database details do not match existing details. Clearing existing."
$searchSvcInstance.SearchDatabase = $null
$searchSvcInstance.Update()
Write-Host "Deleting {$($searchDb.DatabaseConnectionString)}..."
$searchDb.Delete()
Write-Host "Finished deleting search DB."
$searchDb = $null
} else {
Write-Host "Existing Database details match provided details ($($builder2))"
}
}
#If we don't have a DB go ahead and create one.
if ($searchDb -eq $null) {
$dbCreated = $false
try
{
Write-Host "Creating new search database {$builder1}..."
$searchDb = [Microsoft.SharePoint.Search.Administration.SPSearchDatabase]::Create($builder1)
Write-Host "Provisioning new search database..."
$searchDb.Provision()
Write-Host "Provisioning search database complete."
$dbCreated = $true
#Re-get the service to avoid update conflicts
$searchSvc = Get-SPSearchService
$searchSvcInstance = Get-SPSearchServiceInstance -Local
Write-Host "Associating new database with search service instance..."
$searchSvcInstance.SearchDatabase = $searchDb
Write-Host "Updating search service instance..."
$searchSvcInstance.Update()
#Re-get the service to avoid update conflicts
$searchSvc = Get-SPSearchService
$searchSvcInstance = Get-SPSearchServiceInstance -Local
}
catch
{
if ($searchDb -ne $null -and $dbCreated) {
Write-Warning "An error occurred updating the search service instance, deleting search database..."
try
{
#Clean up
$searchDb.Delete()
}
catch
{
Write-Warning "Unable to delete search database."
Write-Error $_
}
}
throw $_
}
}
#Set the database failover server
if (![string]::IsNullOrEmpty($failoverDbServer)) {
if (($searchDb.FailoverServiceInstance -eq $null) -or `
![string]::Equals($searchDb.FailoverServiceInstance.NormalizedDataSource, $failoverDbServer, [StringComparison]::OrdinalIgnoreCase))
{
try
{
Write-Host "Adding failover database instance..."
$searchSvcInstance.SearchDatabase.AddFailoverServiceInstance($failoverDbServer)
Write-Host "Updating search service instance..."
$searchSvcInstance.Update()
}
catch
{
Write-Warning "Unable to set failover database server. $_"
}
}
}
$status = $searchSvcInstance.Status
#Provision the service instance on the current server
if ($status -ne [Microsoft.SharePoint.Administration.SPObjectStatus]::Online) {
if ([Microsoft.SharePoint.Administration.SPServer]::Local -eq $searchSvcInstance.Server) {
try
{
Write-Host "Provisioning search service instance..."
$searchSvcInstance.Provision()
}
catch
{
Write-Warning "The call to SPSearchServiceInstance.Provision (server '$($searchSvcInstance.Server.Name)') failed. Setting back to previous status '$status'. $($_.Exception)"
if ($status -ne $searchSvcInstance.Status) {
try
{
$searchSvcInstance.Status = $status
$searchSvcInstance.Update()
}
catch
{
Write-Warning "Failed to restore previous status on the SPSearchServiceInstance (server '$($searchSvcInstance.Server.Name)'). $($_.Exception)"
}
}
throw $_
}
}
}
#Re-get the service to avoid update conflicts
$searchSvc = Get-SPSearchService
#Create the timer job to update the instances for the other servers.
foreach ($serviceInstance in $searchSvc.Instances) {
if ($serviceInstance -is [Microsoft.SharePoint.Search.Administration.SPSearchServiceInstance] `
-and $serviceInstance -ne $searchSvcInstance `
-and $serviceInstance.Status -eq [Microsoft.SharePoint.Administration.SPObjectStatus]::Online) {
$definition = $serviceInstance.Farm.GetObject("job-service-instance-$($serviceInstance.Id.ToString())", $serviceInstance.Farm.TimerService.Id, [Microsoft.SharePoint.Administration.SPServiceInstanceJobDefinition])
if ($definition -ne $null) {
Write-Host "A provisioning job for the SPSearchServiceInstance on server '$($serviceInstance.Server.Name)' already exists; skipping."
} else {
Write-Host "Creating provisioning job for the SPSearchServiceInstance on server '$($serviceInstance.Server.Name)'..."
$job = New-Object Microsoft.SharePoint.Administration.SPServiceInstanceJobDefinition($serviceInstance, $true)
$job.Update($true)
}
}
}
#Set the proxy type for all the service instances.
$svcConfig.Servers.Server | ForEach-Object {
$server = $_
$instance = Get-SPSearchServiceInstance | where {$_.Server.Name -eq $server.Name}
if ($instance -ne $null `
-and $server.ProxyType.ToLowerInvariant() -ne $instance.ProxyType.ToLowerInvariant) {
Write-Host "Setting proxy type for $($instance.Server.Name) to $($server.ProxyType)..."
$instance | Set-SPSearchServiceInstance -ProxyType $server.ProxyType
}
}
}
One thing you should note is that I'm not setting the schedule for the service. This is because the timer job class that I'd need to use to set the schedule is marked internal thus making it impossible for me to set the schedule without using reflection.
As you can see we're in a bit of a conundrum with SharePoint 2010 - scripting your installations is considered to be a best practice and you should strive to do so whenever possible but the level of complexity involved with scripting such simple things has made it prohibitively complex for the average administrator to do.
I recognized this issue the very first day I started working with SharePoint 2010 and to solve the problem I've been working on a product for ShareSquared called SharePoint Composer which will allow administrators, architects, and developers to visually design their SharePoint configurations and then build out the entire Farm using the model they create in the design tool. This tool will allow you to enforce your corporate standards by clearly documenting every configuration and building the farm based on those configurations in a single-click, automated way - all without having to know any PowerShell at all! Keep a watch here for more information about SharePoint Composer.
Note - I've not had a chance to test this in a multi-server farm so if anyone can give me some feedback about their experiences with it I'd greatly appreciate it.
Creating a SharePoint 2010 Enterprise Search Service Application using PowerShell
The information in this post is specific to SharePoint 2010 Beta 2 and may need adjusting for the RTM version.
In an effort to continue with my previous posts where I demonstrated how to build a basic farm and it's site structure using XML configuration files and PowerShell for SharePoint 2010 I would like to now share how to create a search service application. An automated install of the service applications is, without a doubt, the most difficult PowerShell task you'll undertake when scripting your SharePoint 2010 install, specifically the search application is the most difficult which is why I've chosen to explain it first as I expect it to be one of the most needed and one of the least understood. Note that I'm not planning on giving any depth to what the various components are, there's plenty of other resources that will explain what the admin component is, for example.
To start off let's look at the XML file that will drive our setup. Like my previous examples I have a fairly simplistic XML structure that drives all my configurations. This structure allows me to create as many service application instances as needed, each with their own configurations:
<Services>
<EnterpriseSearchService ContactEmail="no-reply@sp2010.com"
ConnectionTimeout="60"
AcknowledgementTimeout="60"
ProxyType="Default"
IgnoreSSLWarnings="false"
InternetIdentity="Mozilla/4.0 (compatible; MSIE 4.01; Windows NT; MS Search 6.0 Robot)"
IndexLocation="c:\sharepoint\indexes"
PerformanceLevel="PartlyReduced"
Account="sp2010\spsearch">
<EnterpriseSearchServiceApplications>
<EnterpriseSearchServiceApplication Name="Enterprise Search Service Application"
DatabaseServer="spsql1"
DatabaseName="SharePoint_Search"
FailoverDatabaseServer=""
Partitioned="false"
Partitions="1"
SearchServiceApplicationType="Regular">
<ApplicationPool Name="SharePoint Enterprise Search Application Pool" Account="sp2010\spsearch" />
<CrawlServers>
<Server Name="sp2010b2" />
</CrawlServers>
<QueryServers>
<Server Name="sp2010b2" />
</QueryServers>
<SearchQueryAndSiteSettingsServers>
<Server Name="sp2010b2" />
</SearchQueryAndSiteSettingsServers>
<AdminComponent>
<Server Name="sp2010b2" />
<ApplicationPool Name="SharePoint Enterprise Search Application Pool" Account="sp2010\spsearchsvc" />
</AdminComponent>
<Proxy Name="Enterprise Search Service Application Proxy" Partitioned="false">
<ProxyGroup Name="Default" />
</Proxy>
</EnterpriseSearchServiceApplication>
</EnterpriseSearchServiceApplications>
</EnterpriseSearchService>
</Services>
Examining the structure above you can see that I chose to put the <EnterpriseSearchService /> element under a <Services /> element - this will allow me to have all my service configurations in one file rather than a separate file for each service (note that there can be only one <EnterpriseSearchService /> element). Under the <EnterpriseSearchService /> element I have a container element for the applications - there should be only one <EnterpriseSearchServiceApplications /> elements but you can have as many <EnterpriseSearchServiceApplication /> elements under it. The application element is where all the meat of the configurations are. Within this element you define the application pool to use, the crawl and query servers to use, and the server for the administrative component, and finally the proxy definition and it's proxy group memberships. The <CrawlServers /> and <QueryServers /> elements can have as many <Server /> child elements as needed but the <AdminComponent /> element can have only one <Server /> child element. And finally the <Proxy /> element can have as many <ProxyGroup /> child elements as desired.
Okay, so that's the easy part - hopefully you can begin to see the power and flexibility of this simple XML file. No for the scripts - first we need to look at a couple of helper functions, one to get/create our application pools and another for the proxy group memberships. Let's take a look at the application pool function which I called Get-ApplicationPool:
function Get-ApplicationPool([System.Xml.XmlElement]$appPoolConfig) {
#Try and get the application pool if it already exists
$pool = Get-SPIisWebServiceApplicationPool -Identity $appPoolConfig.Name -ErrorVariable err -ErrorAction SilentlyContinue
if ($err) {
#The application pool does not exist so create.
Write-Host "Getting $($appPoolConfig.Account) account for application pool..."
$managedAccount = (Get-SPManagedAccount -Identity $appPoolConfig.Account -ErrorVariable err -ErrorAction SilentlyContinue)
if ($err) {
$accountCred = Get-Credential $appPoolConfig.Account
$managedAccount = New-SPManagedAccount -Credential $accountCred
}
Write-Host "Creating application pool $($appPoolConfig.Name)..."
$pool = New-SPIisWebServiceApplicationPool -Name $appPoolConfig.Name -Account $managedAccount
}
return $pool
}
In this function I'm attempting to get the application pool if it already exists and if it doesn't then I proceed to attempt to get the managed account that will be associated with the application pool. If the managed account doesn't exist then I prompt for credentials and then create the managed account which I then use to create the application pool which gets returned to the calling function.
The next function, which I've named Set-ProxyGroupMembership associates my service application proxy with one or more proxy groups:
function Set-ProxyGroupsMembership([System.Xml.XmlElement[]]$groups, [Microsoft.SharePoint.Administration.SPServiceApplicationProxy[]]$InputObject)
{
begin {}
process {
$proxy = $_
#Clear any existing proxy group assignments
Get-SPServiceApplicationProxyGroup | where {$_.Proxies -contains $proxy} | ForEach-Object {
$proxyGroupName = $_.Name
if ([string]::IsNullOrEmpty($proxyGroupName)) { $proxyGroupName = "Default" }
$group = $null
[bool]$matchFound = $false
foreach ($g in $groups) {
$group = $g.Name
if ($group -eq $proxyGroupName) {
$matchFound = $true
break
}
}
if (!$matchFound) {
Write-Host "Removing ""$($proxy.DisplayName)"" from ""$proxyGroupName"""
$_ | Remove-SPServiceApplicationProxyGroupMember -Member $proxy -Confirm:$false -ErrorAction SilentlyContinue
}
}
foreach ($g in $groups) {
$group = $g.Name
$pg = $null
if ($group -eq "Default" -or [string]::IsNullOrEmpty($group)) {
$pg = [Microsoft.SharePoint.Administration.SPServiceApplicationProxyGroup]::Default
} else {
$pg = Get-SPServiceApplicationProxyGroup $group -ErrorAction SilentlyContinue -ErrorVariable err
if ($pg -eq $null) {
$pg = New-SPServiceApplicationProxyGroup -Name $name
}
}
$pg = $pg | where {$_.Proxies -notcontains $proxy}
if ($pg -ne $null) {
Write-Host "Adding ""$($proxy.DisplayName)"" to ""$($pg.DisplayName)"""
$pg | Add-SPServiceApplicationProxyGroupMember -Member $proxy
}
}
}
end {}
}
This function is probably a bit more complicated than it needs to be but I'm going to use it with every service application script so I'll explain it briefly here and just reference this post in my future posts. For this function I wanted to be able to pass the proxy object that I created into the function using the pipeline rather than a parameter (it just flowed better that way and allowed me to pass more than one proxy if I desired without having to write a loop within the function). The first thing I'm doing in this function is clearing out any existing proxy group assignments that may have been set automatically but are not what I want per the XML file. Once I've cleared undesired assignments then I add any missing assignments. Some service applications will automatically add the proxy to the default proxy group which may not be what you want.
Now that we have our two helper functions out of the way we can start looking at the core function. I'll talk about it in chunks and then at the end of this post provide the complete function.
The first thing I do is load the XML file to a variable, $svcConfig, which I use throughout the function:
1: [xml]$config = Get-Content $settingsFile
2: $svcConfig = $config.Services.EnterpriseSearchService
Line 1 loads the file into a System.Xml.XmlDocument typed variable and then I grab the <EnterpriseSearchService /> element and set that to the $svcConfig variable. Next I need to get the search service itself and set that to a variable which I'll use throughout the function as well. I pass the -Local switch in to get the service instance on the current machien. If I'm unable to find a service instance then something is wrong and I throw an error:
1: $searchSvc = Get-SPEnterpriseSearchServiceInstance -Local
2: if ($searchSvc -eq $null) {
3: throw "Unable to retrieve search service."
4: }
Next I need to get the managed account that will be used for the search service. I first try to retrieve the account in case it already exists and if it doesn't exist then I create after asking the user for the password:
1: Write-Host "Getting $($svcConfig.Account) account for search service..."
2: $searchSvcManagedAccount = (Get-SPManagedAccount -Identity $svcConfig.Account -ErrorVariable err -ErrorAction SilentlyContinue)
3: if ($err) {
4: $searchSvcAccount = Get-Credential $svcConfig.Account
5: $searchSvcManagedAccount = New-SPManagedAccount -Credential $searchSvcAccount
6: }
Now that we have a managed account and service instance we can set the core properties for the search service. I end up doing this on every machine but it only needs to be done once - just easier to set it every time rather than try and figure out if it's been set yet and doing so has no negative repercussions:
1: Get-SPEnterpriseSearchService | Set-SPEnterpriseSearchService `
2: -ServiceAccount $searchSvcManagedAccount.Username `
3: -ServicePassword $searchSvcManagedAccount.SecurePassword `
4: -ContactEmail $svcConfig.ContactEmail `
5: -ConnectionTimeout $svcConfig.ConnectionTimeout `
6: -AcknowledgementTimeout $svcConfig.AcknowledgementTimeout `
7: -ProxyType $svcConfig.ProxyType `
8: -IgnoreSSLWarnings $svcConfig.IgnoreSSLWarnings `
9: -InternetIdentity $svcConfig.InternetIdentity `
10: -PerformanceLevel $svcConfig.PerformanceLevel
11:
12: Write-Host "Setting default index location on search service..."
13: $searchSvc | Set-SPEnterpriseSearchServiceInstance -DefaultIndexLocation $svcConfig.IndexLocation -ErrorAction SilentlyContinue -ErrorVariable err
The core service settings are in place, now it's time to create all the service applications. In the example XML we have just one but we could have more so I use the ForEach-Object cmdlet to loop through all the definitions:
1: $svcConfig.EnterpriseSearchServiceApplications.EnterpriseSearchServiceApplication | ForEach-Object {
The first thing we need to do to create our app is to create the application pool for the service application itself and the administration component:
1: $appConfig = $_
2:
3: #Try and get the application pool if it already exists
4: $pool = Get-ApplicationPool $appConfig.ApplicationPool
5: $adminPool = Get-ApplicationPool $appConfig.AdminComponent.ApplicationPool
Before creating the application pools I store the current XML element in the $appConfig node for easier reference and to avoid conflicts with sub-loops. I then call the helper function I showed earlier to create the two application pools which I'll use later. Next I check to see if the service application has already been created (line 1 below) by calling Get-SPEnterpriseSearchServiceApplication and if it does not exist then I create a new one. This helps when you have to run the script again due to possible errors that may occur later in the script (I've often seen update conflict errors occur randomly, running the script again is usually all that's necessary):
1: $searchApp = Get-SPEnterpriseSearchServiceApplication -Identity $appConfig.Name -ErrorAction SilentlyContinue
2: if ($searchApp -eq $null) {
3: Write-Host "Creating enterprise search service application..."
4: $searchApp = New-SPEnterpriseSearchServiceApplication -Name $appConfig.Name `
5: -DatabaseServer $appConfig.DatabaseServer `
6: -DatabaseName $appConfig.DatabaseName `
7: -FailoverDatabaseServer $appConfig.FailoverDatabaseServer `
8: -ApplicationPool $pool `
9: -AdminApplicationPool $adminPool `
10: -Partitioned:([bool]::Parse($appConfig.Partitioned)) `
11: -SearchApplicationType $appConfig.SearchServiceApplicationType
12: } else {
13: Write-Host "Enterprise search service application already exists, skipping creation."
14: }
Now that the service application exists we can go ahead and create the proxy and set the proxy group memberships:
1: $proxy = Get-SPEnterpriseSearchServiceApplicationProxy -Identity $appConfig.Proxy.Name -ErrorAction SilentlyContinue
2: if ($proxy -eq $null) {
3: Write-Host "Creating enterprise search service application proxy..."
4: $proxy = New-SPEnterpriseSearchServiceApplicationProxy -Name $appConfig.Proxy.Name -SearchApplication $searchApp -Partitioned:([bool]::Parse($appConfig.Proxy.Partitioned))
5: } else {
6: Write-Host "Enterprise search service application proxy already exists, skipping creation."
7: }
8: if ($proxy.Status -ne "Online") {
9: $proxy.Status = "Online"
10: $proxy.Update()
11: }
12: $proxy | Set-ProxyGroupsMembership $appConfig.Proxy.ProxyGroup
Like with the service application I first try and get the proxy in case it has already been created and if I don't find it then I create it. Once I have a reference to the proxy object I check to see if it's online and if not then I set it online and call Update() to commit the change. And finally I call the Set-ProxyGroupsMembership function that I previously defined.
The intent of the script is to allow it to be run on multiple servers to support a multi-server scripted deployment. That's where this next bit comes in:
1: $installCrawlSvc = (($appConfig.CrawlServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
2: $installQuerySvc = (($appConfig.QueryServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
3: $installAdminCmpnt = (($appConfig.AdminComponent.Server | where {$_.Name -eq $env:computername}) -ne $null)
4: $installSyncSvc = (($appConfig.SearchQueryAndSiteSettingsServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
For both the crawl servers, query servers, and admin component I get the name of the current computer ($env:computername) and then check to see if an <Server /> element has been declared with a matching name for the specific component. The variables declared are then used throughout the rest of the script.
Before I can create the crawl or query component I need start search service instance that we previously acquired:
1: if ($searchSvc.Status -ne "Online" -and ($installCrawlSvc -or $installQuerySvc)) {
2: $searchSvc | Start-SPEnterpriseSearchServiceInstance
3: }
If the service isn't already online and if we're on an appropriate server then I start the service by passing the service instance to the Start-SPEnterpriseSearchServiceInstance cmdlet. Next I need to set the administration component:
1: if ($installAdminCmpnt) {
2: Write-Host "Setting administration component..."
3: Set-SPEnterpriseSearchAdministrationComponent -SearchApplication $searchApp -SearchServiceInstance $searchSvc
4: }
The trick with this bit is that you have to set the administration component before you can set the query or crawl components so the first time you run this script it must be on the sever that is to run the administration component - short of having the user run the script multiple times on the same server and adding appropriate code to handle that I've not come up with any way around this - frankly, it sucks, big time - so be careful with this one!
Okay, we're about halfway through, still with me?
Now it's time to create the crawl topology:
1: $crawlTopology = Get-SPEnterpriseSearchCrawlTopology -SearchApplication $searchApp | where {$_.CrawlComponents.Count -gt 0 -or $_.State -eq "Inactive"}
2: if ($crawlTopology -eq $null) {
3: Write-Host "Creating new crawl topology..."
4: $crawlTopology = $searchApp | New-SPEnterpriseSearchCrawlTopology
5: } else {
6: Write-Host "A crawl topology with crawl components already exists, skipping crawl topology creation."
7: }
8:
9: if ($installCrawlSvc) {
10: $crawlComponent = $crawlTopology.CrawlComponents | where {$_.ServerName -eq $env:ComputerName}
11: if ($crawlTopology.CrawlComponents.Count -eq 0 -and $crawlComponent -eq $null) {
12: $crawlStore = $searchApp.CrawlStores | where {$_.Name -eq "$($appConfig.DatabaseName)_CrawlStore"}
13: Write-Host "Creating new crawl component..."
14: $crawlComponent = New-SPEnterpriseSearchCrawlComponent -SearchServiceInstance $searchSvc -SearchApplication $searchApp -CrawlTopology $crawlTopology -CrawlDatabase $crawlStore.Id.ToString() -IndexLocation $appConfig.IndexLocation
15: } else {
16: Write-Host "Crawl component already exist, skipping crawl component creation."
17: }
18: }
On line 1 I'm getting all existing crawl topologies for the service application (Get-SPEnterpriseSearchCrawlTopology) and filtering on whether or not the crawl topology has components and is active or not. I do this because when the search application is created it automatically creates a crawl topology for us but that topology is not configured correctly (there are no crawl components) but once the topology has been made active it doesn't let us change it in order to add crawl components. When I create our new topology it will be inactive so I will use this fact when I run the script on the next server. Once I have the crawl topology I can then add the crawl components using the New-SPEnterpriseSearchCrawlComponent cmdlet (note that you have to pass in the crawl store ID so I have to get that ID as shown in line 12).
After we create crawl topology and components we do essentially the exact same thing for the query topology and components:
1: $queryTopology = Get-SPEnterpriseSearchQueryTopology -SearchApplication $searchApp | where {$_.QueryComponents.Count -gt 0 -or $_.State -eq "Inactive"}
2: if ($queryTopology -eq $null) {
3: Write-Host "Creating new query topology..."
4: $queryTopology = $searchApp | New-SPEnterpriseSearchQueryTopology -Partitions $appConfig.Partitions
5: } else {
6: Write-Host "A query topology with query components already exists, skipping query topology creation."
7: }
8: if ($installQuerySvc) {
9: $queryComponent = $queryTopology.QueryComponents | where {$_.ServerName -eq $env:ComputerName}
10: if ($queryTopology.QueryComponents.Count -eq 0 -and $queryComponent -eq $null) {
11: $partition = ($queryTopology | Get-SPEnterpriseSearchIndexPartition)
12: Write-Host "Creating new query component..."
13: $queryComponent = New-SPEnterpriseSearchQueryComponent -IndexPartition $partition -QueryTopology $queryTopology -SearchServiceInstance $searchSvc
14: Write-Host "Setting index partition and property store database..."
15: $propertyStore = $searchApp.PropertyStores | where {$_.Name -eq "$($appConfig.DatabaseName)_PropertyStore"}
16: $partition | Set-SPEnterpriseSearchIndexPartition -PropertyDatabase $propertyStore.Id.ToString()
17: } else {
18: Write-Host "Query component already exist, skipping query component creation."
19: }
20: }
Great! We have our admin component created, our crawl topology and components created, and our query topology and components created. Now we just need to make things active. There's nothing more to do with the admin component so we'll first start the "Search Query and Site Settings Service" and then continue with the crawl topology:
1: if ($installSyncSvc) {
2: Start-SPServiceInstance -Identity "Search Query and Site Settings Service"
3: }
So starting the query and site settings service was easy, now lets move on to the hard stuff:
1: #Don't activate until we've added all components
2: $allCrawlServersDone = $true
3: $appConfig.CrawlServers.Server | ForEach-Object {
4: $server = $_.Name
5: $top = $crawlTopology.CrawlComponents | where {$_.ServerName -eq $server}
6: if ($top -eq $null) { $allCrawlServersDone = $false }
7: }
8:
9: if ($allCrawlServersDone -and $crawlTopology.State -ne "Active") {
10: Write-Host "Setting new crawl topology to active..."
11: $crawlTopology | Set-SPEnterpriseSearchCrawlTopology -Active -Confirm:$false
12:
13: Write-Host -ForegroundColor Yellow "Waiting on Crawl Components to provision..."
14: while ($true) {
15: $ct = Get-SPEnterpriseSearchCrawlTopology -Identity $crawlTopology -SearchApplication $searchApp
16: $state = $ct.CrawlComponents | where {$_.State -ne "Ready"}
17: if ($ct.State -eq "Active" -and $state -eq $null) {
18: break
19: }
20: Write-Host -ForegroundColor Yellow "Waiting on Crawl Components to provision..."
21: Start-Sleep 2
22: }
23: # Need to delete the original crawl topology that was created by default
24: $searchApp | Get-SPEnterpriseSearchCrawlTopology | where {$_.State -eq "Inactive"} | Remove-SPEnterpriseSearchCrawlTopology -Confirm:$false
25: }
The first thing I do is set a variable to indicate whether I've gotten all designated crawl servers configured - we don't want to set the crawl topology active until all the servers have been configured because once we make it active we can't change it (this is critical if you are planning on doing a phased server roll-out - you will need to rebuild your topology if you need to add additional crawl or query components). On line 11 I set the topology as active using the Set-SPEnterpriseSearchCrawlTopology cmdlet. Problem is not quite that simple - you see, this cmdlet runs asynchronously, meaning that it returns immediately and does not wait until the service is made active - this is critical because we can't proceed to the query piece until the crawl topology is active so all I'm doing in lines 14 through 22 is checking the status and if it's not "Ready" then I sleep for 2 seconds and try again.
Only one more thing - now that the crawl topology is active we do, once again, the same thing for the query topology:
1: $allQueryServersDone = $true
2: $appConfig.QueryServers.Server | ForEach-Object {
3: $server = $_.Name
4: $top = $queryTopology.QueryComponents | where {$_.ServerName -eq $server}
5: if ($top -eq $null) { $allQueryServersDone = $false }
6: }
7:
8: #Make sure we have a crawl component added and started before trying to enable the query component
9: if ($allCrawlServersDone -and $allQueryServersDone -and $queryTopology.State -ne "Active") {
10: Write-Host "Setting query topology as active..."
11: $queryTopology | Set-SPEnterpriseSearchQueryTopology -Active -Confirm:$false -ErrorAction SilentlyContinue -ErrorVariable err
12:
13: Write-Host -ForegroundColor Yellow "Waiting on Query Components to provision..."
14: while ($true) {
15: $qt = Get-SPEnterpriseSearchQueryTopology -Identity $queryTopology -SearchApplication $searchApp
16: $state = $qt.QueryComponents | where {$_.State -ne "Ready"}
17: if ($qt.State -eq "Active" -and $state -eq $null) {
18: break
19: }
20: Write-Host -ForegroundColor Yellow "Waiting on Query Components to provision..."
21: Start-Sleep 2
22: }
23: # Need to delete the original query topology that was created by default
24: $searchApp | Get-SPEnterpriseSearchQueryTopology | where {$_.State -eq "Inactive"} | Remove-SPEnterpriseSearchQueryTopology -Confirm:$false
25: }
This code is identical to that of the crawl topology but uses the query specific cmdlets.
And, finally, after about 236 lines of code, we're done! Makes me miss the days of MOSS 2007 where I could start search with one line of STSADM (maybe I need to create a Start-OSearch cmdlet
). So, putting it all together, here's the complete function:
1: function Start-EnterpriseSearch([string]$settingsFile = "Configurations.xml") {
2: [xml]$config = Get-Content $settingsFile
3: $svcConfig = $config.Services.EnterpriseSearchService
4:
5: $searchSvc = Get-SPEnterpriseSearchServiceInstance -Local
6: if ($searchSvc -eq $null) {
7: throw "Unable to retrieve search service."
8: }
9:
10: Write-Host "Getting $($svcConfig.Account) account for search service..."
11: $searchSvcManagedAccount = (Get-SPManagedAccount -Identity $svcConfig.Account -ErrorVariable err -ErrorAction SilentlyContinue)
12: if ($err) {
13: $searchSvcAccount = Get-Credential $svcConfig.Account
14: $searchSvcManagedAccount = New-SPManagedAccount -Credential $searchSvcAccount
15: }
16:
17: Get-SPEnterpriseSearchService | Set-SPEnterpriseSearchService `
18: -ServiceAccount $searchSvcManagedAccount.Username `
19: -ServicePassword $searchSvcManagedAccount.SecurePassword `
20: -ContactEmail $svcConfig.ContactEmail `
21: -ConnectionTimeout $svcConfig.ConnectionTimeout `
22: -AcknowledgementTimeout $svcConfig.AcknowledgementTimeout `
23: -ProxyType $svcConfig.ProxyType `
24: -IgnoreSSLWarnings $svcConfig.IgnoreSSLWarnings `
25: -InternetIdentity $svcConfig.InternetIdentity `
26: -PerformanceLevel $svcConfig.PerformanceLevel
27:
28: Write-Host "Setting default index location on search service..."
29: $searchSvc | Set-SPEnterpriseSearchServiceInstance -DefaultIndexLocation $svcConfig.IndexLocation -ErrorAction SilentlyContinue -ErrorVariable err
30:
31: $svcConfig.EnterpriseSearchServiceApplications.EnterpriseSearchServiceApplication | ForEach-Object {
32: $appConfig = $_
33:
34: #Try and get the application pool if it already exists
35: $pool = Get-ApplicationPool $appConfig.ApplicationPool
36: $adminPool = Get-ApplicationPool $appConfig.AdminComponent.ApplicationPool
37:
38: $searchApp = Get-SPEnterpriseSearchServiceApplication -Identity $appConfig.Name -ErrorAction SilentlyContinue
39: if ($searchApp -eq $null) {
40: Write-Host "Creating enterprise search service application..."
41: $searchApp = New-SPEnterpriseSearchServiceApplication -Name $appConfig.Name `
42: -DatabaseServer $appConfig.DatabaseServer `
43: -DatabaseName $appConfig.DatabaseName `
44: -FailoverDatabaseServer $appConfig.FailoverDatabaseServer `
45: -ApplicationPool $pool `
46: -AdminApplicationPool $adminPool `
47: -Partitioned:([bool]::Parse($appConfig.Partitioned)) `
48: -SearchApplicationType $appConfig.SearchServiceApplicationType
49: } else {
50: Write-Host "Enterprise search service application already exists, skipping creation."
51: }
52:
53: $installCrawlSvc = (($appConfig.CrawlServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
54: $installQuerySvc = (($appConfig.QueryServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
55: $installAdminCmpnt = (($appConfig.AdminComponent.Server | where {$_.Name -eq $env:computername}) -ne $null)
56: $installSyncSvc = (($appConfig.SearchQueryAndSiteSettingsServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
57:
58: if ($searchSvc.Status -ne "Online" -and ($installCrawlSvc -or $installQuerySvc)) {
59: $searchSvc | Start-SPEnterpriseSearchServiceInstance
60: }
61:
62: if ($installAdminCmpnt) {
63: Write-Host "Setting administration component..."
64: Set-SPEnterpriseSearchAdministrationComponent -SearchApplication $searchApp -SearchServiceInstance $searchSvc
65: }
66:
67: $crawlTopology = Get-SPEnterpriseSearchCrawlTopology -SearchApplication $searchApp | where {$_.CrawlComponents.Count -gt 0 -or $_.State -eq "Inactive"}
68: if ($crawlTopology -eq $null) {
69: Write-Host "Creating new crawl topology..."
70: $crawlTopology = $searchApp | New-SPEnterpriseSearchCrawlTopology
71: } else {
72: Write-Host "A crawl topology with crawl components already exists, skipping crawl topology creation."
73: }
74:
75: if ($installCrawlSvc) {
76: $crawlComponent = $crawlTopology.CrawlComponents | where {$_.ServerName -eq $env:ComputerName}
77: if ($crawlTopology.CrawlComponents.Count -eq 0 -and $crawlComponent -eq $null) {
78: $crawlStore = $searchApp.CrawlStores | where {$_.Name -eq "$($appConfig.DatabaseName)_CrawlStore"}
79: Write-Host "Creating new crawl component..."
80: $crawlComponent = New-SPEnterpriseSearchCrawlComponent -SearchServiceInstance $searchSvc -SearchApplication $searchApp -CrawlTopology $crawlTopology -CrawlDatabase $crawlStore.Id.ToString() -IndexLocation $appConfig.IndexLocation
81: } else {
82: Write-Host "Crawl component already exist, skipping crawl component creation."
83: }
84: }
85:
86: $queryTopology = Get-SPEnterpriseSearchQueryTopology -SearchApplication $searchApp | where {$_.QueryComponents.Count -gt 0 -or $_.State -eq "Inactive"}
87: if ($queryTopology -eq $null) {
88: Write-Host "Creating new query topology..."
89: $queryTopology = $searchApp | New-SPEnterpriseSearchQueryTopology -Partitions $appConfig.Partitions
90: } else {
91: Write-Host "A query topology with query components already exists, skipping query topology creation."
92: }
93: if ($installQuerySvc) {
94: $queryComponent = $queryTopology.QueryComponents | where {$_.ServerName -eq $env:ComputerName}
95: if ($queryTopology.QueryComponents.Count -eq 0 -and $queryComponent -eq $null) {
96: $partition = ($queryTopology | Get-SPEnterpriseSearchIndexPartition)
97: Write-Host "Creating new query component..."
98: $queryComponent = New-SPEnterpriseSearchQueryComponent -IndexPartition $partition -QueryTopology $queryTopology -SearchServiceInstance $searchSvc
99: Write-Host "Setting index partition and property store database..."
100: $propertyStore = $searchApp.PropertyStores | where {$_.Name -eq "$($appConfig.DatabaseName)_PropertyStore"}
101: $partition | Set-SPEnterpriseSearchIndexPartition -PropertyDatabase $propertyStore.Id.ToString()
102: } else {
103: Write-Host "Query component already exist, skipping query component creation."
104: }
105: }
106:
107: if ($installSyncSvc) {
108: Start-SPServiceInstance -Identity "Search Query and Site Settings Service"
109: }
110:
111: #Don't activate until we've added all components
112: $allCrawlServersDone = $true
113: $appConfig.CrawlServers.Server | ForEach-Object {
114: $server = $_.Name
115: $top = $crawlTopology.CrawlComponents | where {$_.ServerName -eq $server}
116: if ($top -eq $null) { $allCrawlServersDone = $false }
117: }
118:
119: if ($allCrawlServersDone -and $crawlTopology.State -ne "Active") {
120: Write-Host "Setting new crawl topology to active..."
121: $crawlTopology | Set-SPEnterpriseSearchCrawlTopology -Active -Confirm:$false
122:
123: Write-Host -ForegroundColor Yellow "Waiting on Crawl Components to provision..."
124: while ($true) {
125: $ct = Get-SPEnterpriseSearchCrawlTopology -Identity $crawlTopology -SearchApplication $searchApp
126: $state = $ct.CrawlComponents | where {$_.State -ne "Ready"}
127: if ($ct.State -eq "Active" -and $state -eq $null) {
128: break
129: }
130: Write-Host -ForegroundColor Yellow "Waiting on Crawl Components to provision..."
131: Start-Sleep 2
132: }
133: # Need to delete the original crawl topology that was created by default
134: $searchApp | Get-SPEnterpriseSearchCrawlTopology | where {$_.State -eq "Inactive"} | Remove-SPEnterpriseSearchCrawlTopology -Confirm:$false
135: }
136:
137: $allQueryServersDone = $true
138: $appConfig.QueryServers.Server | ForEach-Object {
139: $server = $_.Name
140: $top = $queryTopology.QueryComponents | where {$_.ServerName -eq $server}
141: if ($top -eq $null) { $allQueryServersDone = $false }
142: }
143:
144: #Make sure we have a crawl component added and started before trying to enable the query component
145: if ($allCrawlServersDone -and $allQueryServersDone -and $queryTopology.State -ne "Active") {
146: Write-Host "Setting query topology as active..."
147: $queryTopology | Set-SPEnterpriseSearchQueryTopology -Active -Confirm:$false -ErrorAction SilentlyContinue -ErrorVariable err
148:
149: Write-Host -ForegroundColor Yellow "Waiting on Query Components to provision..."
150: while ($true) {
151: $qt = Get-SPEnterpriseSearchQueryTopology -Identity $queryTopology -SearchApplication $searchApp
152: $state = $qt.QueryComponents | where {$_.State -ne "Ready"}
153: if ($qt.State -eq "Active" -and $state -eq $null) {
154: break
155: }
156: Write-Host -ForegroundColor Yellow "Waiting on Query Components to provision..."
157: Start-Sleep 2
158: }
159: # Need to delete the original query topology that was created by default
160: $searchApp | Get-SPEnterpriseSearchQueryTopology | where {$_.State -eq "Inactive"} | Remove-SPEnterpriseSearchQueryTopology -Confirm:$false
161: }
162:
163: $proxy = Get-SPEnterpriseSearchServiceApplicationProxy -Identity $appConfig.Proxy.Name -ErrorAction SilentlyContinue
164: if ($proxy -eq $null) {
165: Write-Host "Creating enterprise search service application proxy..."
166: $proxy = New-SPEnterpriseSearchServiceApplicationProxy -Name $appConfig.Proxy.Name -SearchApplication $searchApp -Partitioned:([bool]::Parse($appConfig.Proxy.Partitioned))
167: } else {
168: Write-Host "Enterprise search service application proxy already exists, skipping creation."
169: }
170: if ($proxy.Status -ne "Online") {
171: $proxy.Status = "Online"
172: $proxy.Update()
173: }
174: $proxy | Set-ProxyGroupsMembership $appConfig.Proxy.ProxyGroup
175:
176: }
177: }
178:
This script took me an incredible amount of time to figure out and I really hope others are able to benefit from it. If you find areas of improvement or anything that requires correction please, please, please post a comment so that I and others can benefit from your experiences with it.
Also, this script is a derivative of a slightly more complex one that I use for all my stuff and though that more complex script has gone through many rounds of testing this one has not - mainly I've not had a chance to test in a multi-server environment and have only had time to do a single server deploy (though the changes related to the servers were very small and, if they were to fail, would likely have failed on the single server). Mainly try to remember that the product is still in beta so you should expect that things may either change between now and RTM or things may just not work from one environment to the next.
Good luck and happy scripting!
Updating the Default Content Access Account via STSADM
This is one that I've been wanting to address for a while and I finally decided to sit down and just do it. If you've had your environment in place long enough to have to change the passwords you know that you can change most of the passwords using the out of the box STSADM commands - many refer to this support article from Microsoft on how to do this: http://support.microsoft.com/kb/934838.
Because there's so many accounts to change and so many places to visit this is definitely one of those things you want to have scripted (just be careful where you store your script). If you look at the article though you'll notice that it doesn't address updating the user profile import account and it mentions that you have to manually change the default content access account. I already have a command to change the user profile import account but I didn't have anything for changing the default content access account and having scripts with manual steps just kind of defeats the purpose in my opinion. So, I created a new command which I called gl-updatedefaultcontentaccessaccount.
Setting the default content access account through code is real easy - you just call the SetDefaultGatheringAccount method of an instance of the Content class which can be obtained by calling the static GetContext method from the SearchContext object:
1: /// <summary>
2: /// Updates the account.
3: /// </summary>
4: /// <param name="sspName">Name of the SSP.</param>
5: /// <param name="user">The user.</param>
6: /// <param name="pwd">The PWD.</param>
7: public static void UpdateAccount(string sspName, string user, string pwd)
8: {
9: ServerContext context;
10: if (string.IsNullOrEmpty(sspName))
11: context = ServerContext.Default;
12: else
13: context = ServerContext.GetContext(sspName);
14:
15: SearchContext parent = SearchContext.GetContext(context);
16:
17: Content content = new Content(parent);
18: try
19: {
20: content.SetDefaultGatheringAccount(user, Utilities.CreateSecureString(pwd));
21: }
22: catch (RemotingException)
23: {
24: throw new Exception("Invalid login.");
25: }
26: catch (COMException)
27: {
28: throw new Exception("Invalid login.");
29: }
30: }
The help for the command is shown below:
C:\>stsadm -help gl-updatedefaultcontentaccessaccount
stsadm -o gl-updatedefaultcontentaccessaccount
Sets the account to use as the default account when crawling content. This account must have read access to the content being crawled. To avoid crawling unpublished versions of documents, ensure that this account is not an administrator on the target server.
Parameters:
[-ssp <SSP name>]
-username <DOMAIN\name>
-password <password>
|
The following table summarizes the command and its various parameters:
| Command Name | Availability | Build Date |
|---|---|---|
| gl-updatedefaultcontentaccessaccount | MOSS 2007 | Released: 8/15/2008 |
| Parameter Name | Short Form | Required | Description | Example Usage |
|---|---|---|---|---|
| ssp | No | The SSP that the account is associated with. If omitted then the default SSP is used. | -ssp SSP1 | |
| username | u | Yes | The username of the account to use. The account must have read access to the content being crawled. To avoid crawling unpublished versions of documents, ensure that the account is not an administrator on the target server. | -username "domain\sspcontent"
-u "domain\sspcontent" |
| password | pwd | Yes | The password associated with the specified username. | -password "pa$$w0rd"
-pwd "pa$$w0rd" |
The following is an example of how to set the default content access account:
stsadm -o gl-updatedefaultcontentaccessaccount -username "domain\sspcontent" -password "pa$$w0rd"
I'll follow up this post with a sample password change script that I use which includes this command.
Search Scopes and Rules
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.
1. 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:
1: public 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.
2. 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:
1: public 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"
3. 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):
1: public 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"
Set Search Center
This was a pretty simple command to create and only took me a few minutes. Basically I needed to be able to set the search center for a site collection. You can do this via the browser by going to http://[portal]/[site collection]/_layouts/enhancedSearch.aspx. The command I created is called gl-setsearchcenter.
The code for this is beyond simple - it was just a matter of setting a string based property via the AllProperties collection:
1: string url = Params["url"].Value;