MOSS MVP

I've moved my blog to http://blog.falchionconsulting.com!. Please update your links. This blog is no longer in use--you can find all posts and comments at my new blog; I will no longer be posting to this site and comments have been disabled.

Saturday, December 19, 2009

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!