Last updated
SharePoint's own search does not give us the facets and ranking an enterprise search experience needs.
A good search page over a service catalogue, a supplier directory or a set of FAQ lists has facets by department and category, filters on the fields people search by, and ranking tuned to what staff look for. SharePoint’s own search returns pages and items, with facets built from whatever values happen to be in the columns. That quality of search needs an index built from clean, current data.
Index the lists directly
SPSync replicates the source lists into Azure AI Search, Elasticsearch or OpenSearch and refreshes the index on a schedule. There is no database step in between; SPSync writes the index itself. Each item becomes a document, and each lookup arrives as a display value and an ID on that document, so a query can facet and filter on clean fields with no nested API shapes to unpick.
| Title | Client lookup | Tags multi-choice | Owner person | Due |
|---|---|---|---|---|
| Riverside | Acme Ltd | Design; Build | Ada Lovelace | 2026-11-30 |
| Harbour | Borealis plc | Build | Alan Turing | 2027-02-14 |
then sync
- id
- title
- client_id → clients.id
- owner_id → sp_users.id
- due
- is_deleted soft delete
- sp_modified
- id
- title
- …
- project_id
- tag
One row per value: Riverside has two.
- id
- display_name
One users table shared by every list.
The documents and their child indexes
Dates arrive as ISO strings. A multi-choice column gets its own child index, one document per item and value, which a query joins back to by item ID. Multi-lookup and multi-person columns use the same design, still under verification on a live tenant. When an item is deleted in SharePoint, its document is removed from the index on the next sync, so results only show content that still exists.
Plan access before launch. On Azure AI Search every list index declares a filterable
permitted_groups field, and SPSync leaves it empty on every document. SharePoint’s
item-level permissions are not copied, so the index is visible to anyone holding a query
key for the service.
Keyword, filtered or hybrid search
Azure AI Search also supports a vector field, for hybrid keyword and vector search over the text columns you choose to embed. Elasticsearch and OpenSearch provide keyword and filtered search. All three ship today, and their pages cover the document shape and setup: SharePoint to Azure AI Search, SharePoint to Elasticsearch and SharePoint to OpenSearch. To run your own indexing over a relational replica, Postgres or SQL Server work as well.
Pick the lists and set the schedule by how quickly a new item should be findable. Which fields are filterable and facetable follows each column’s type, so there is nothing to map by hand. A small internal index can be fed from the desktop app while it runs; a production search page should have its sync somewhere that stays on, and a reindex can be started from CI with the command line.
Desktop, Azure or command line
All three editions run the same engine, write the same tables and use the same licence key, so the choice comes down to who looks after the sync.
Desktop app
For macOS and Windows. Sign in with Microsoft, tick the lists, choose a database and sync from your own machine on the schedule you set. Delegated sign-in needs no admin unless your tenant requires admin consent for apps.
In your Azure
SPSync in your Azure, a managed application for your own subscription, is deployed with you on a call while the Marketplace listing is prepared. Once deployed it is always on, so the sync keeps running with every laptop closed.
Command line
Every replication is a YAML file you can review and commit. Run it from a server, a container or a CI pipeline, and re-run it safely any time.
Set it up
Ready to run this against your own site? The setup guide takes you from install to a first replication, and the SharePoint connection guide covers signing in.
Script it
Everything you can click, you can script. The CLI, YAML config and automation below reproduce this scenario exactly. See the CLI reference.
spsync auth login --client-id 00000000-0000-0000-0000-000000000000
spsync export-config --output replication.yaml --pipeline-name sharepoint_search_index --prefix sharepoint_search_index --destination-kind azure-ai-search --connection-string-env SPSYNC_DB_CONNECTION --dataset-name sharepoint_search_index --client-id 00000000-0000-0000-0000-000000000000 --site-url https://contoso.sharepoint.com/sites/example --all-lists
spsync doctor replication.yaml
spsync run replication.yaml pipeline_name: sharepoint_search_index
prefix: sharepoint_search_index
auth:
mode: delegated
client_id: 00000000-0000-0000-0000-000000000000
tenant_id: common
destination:
kind: azure-ai-search
connection_string_env: SPSYNC_DB_CONNECTION
dataset_name: sharepoint_search_index
scopes:
- site_url: https://contoso.sharepoint.com/sites/example
mode: entire_site
reconcile_interval_days: 7
create_reporting_views: false
Cron (Azure AI Search nightly):
# every night at 02:00
0 2 * * * cd /opt/spsync && SPSYNC_DB_CONNECTION="$(cat /etc/spsync/db.env)" uv run spsync run replication.yaml >> /var/log/spsync/run.log 2>&1 GitHub Actions:
# .github/workflows/spsync-run.yml
name: SPSync
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch: {}
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- run: uv sync
- run: uv run spsync run replication.yaml
env:
SPSYNC_DB_CONNECTION: ${{ secrets.SPSYNC_DB_CONNECTION }}
SPSYNC_LICENCE_KEY: ${{ secrets.SPSYNC_LICENCE_KEY }} Azure DevOps:
# azure-pipelines.yml
schedules:
- cron: "0 2 * * *"
branches: { include: [main] }
always: true
steps:
- script: uv sync
- script: uv run spsync run replication.yaml
env:
SPSYNC_DB_CONNECTION: $(SPSYNC_DB_CONNECTION)
SPSYNC_LICENCE_KEY: $(SPSYNC_LICENCE_KEY) Windows (PowerShell + Task Scheduler):
# Windows Task Scheduler action (see /docs/cli/automation/)
$env:SPSYNC_DB_CONNECTION = (Get-Content C:\spsync\db.env)
uv run spsync run C:\spsync\replication.yaml
schtasks /Create /SC DAILY /ST 02:00 /TN "SPSync" /TR "powershell -File C:\spsync\run.ps1" macOS (launchd):
# ~/Library/LaunchAgents/io.spsync.run.plist, then: launchctl load ~/Library/LaunchAgents/io.spsync.run.plist Secrets (licence key, connection strings) come from the environment, so never write them into the YAML. See scheduling and secrets.