[食譜好菜] 殺雞焉用牛刀,想做關鍵字搜尋 Windows Search Service 一樣嚇嚇叫。

最近手上的專案有一個需求,使用者想對上傳的檔案(簡報、會議記錄、...等)做關鍵字搜尋,腦中閃過的第一個解決方案是 Elasticsearch(ES),但是這得額外架設服務、撰寫程式將檔案內容送進 ES 做索引,要花錢、花時間,使用者不一定買單,所以我就想到 Windows 檔案總管的搜尋功能能不能拿來用?它背後使用的服務是 Windows Search Service(WSS),下關鍵字去 Google 馬上就找到黑大的文章,感謝黑大。

支援的檔案類型

WSS 支援哪些檔案類型?我們可以從 Windows Search 設定中來看查,以 Windows 10 為例,在搜尋框輸入「Windows Search 設定」,點擊打開視窗之後,往下捲找到「進階搜尋索引子設定」點擊它,在打開的視窗中點擊「進階」,在進階選項中的「檔案類型」頁籤我們就可以找到目前有支援的檔案類型。

我們可以看到支援的檔案類型不少,注意到中間有一個選項「應該如何為此種檔案類型建立索引?」,如果我們要對檔案內容做索引,就要選擇「索引檔案屬性和內容」,不過 WSS 已經有針對常見的文字類型副檔名預設選擇該選項,比如說 txt、doc/docx、ptt/pttx、xls/xlsx、pdf、...等,我們想知道哪一個副檔名預設的索引選項是什麼?只要點選該副檔名就可以知道了。

不支援的副檔名我們也可以自己新增,在下面「將新的副檔名新增到清單」的區域中輸入副檔名,點擊「新增」就可以了。

建立索引

建立索引的方式也很簡單,我們先建立一個資料夾,假設叫 Search 好了,建好之後我們回到「索引選項」的視窗,點擊「修改」,將剛剛我們建立的資料夾打勾,接著按「確定」。

再來我們就可以將要建立內容索引的檔案丟進這個資料夾,讓 WSS 建立檔案內容的索引。

※ 在這邊我遇到一個索引內容亂碼的問題,在改用帶有 BOM 的 UTF-8 編碼儲存純文字檔(txt)之後,問題就得到解決了。

關鍵字搜尋

到這邊我們可以開始做關鍵字搜尋了,我們先在檔案總管試著下關鍵字搜尋看看,沒問題的話應該是會成功的。

接下來我們用 C# 撰寫搜尋關鍵字的程式,要對 WSS 做關鍵字搜尋,我們可以選擇我們比較熟悉的 SQL 語法的方式,要 Query 的對象是 SystemIndex,而要 SELECT 的欄位可以從 Property Mappings (from WDS 2.x to 3.x) 這篇文章去參考,不過它也沒有列出所有可 SELECT 的欄位,剩下的只能透過範例或是其他大大的經驗去得知。

WHERE 條件的部分,我們可以用 DIRECTORYSCOPE 去指定要搜尋的資料夾,差別在於 SCOPE 會搜尋子資料夾,關鍵字則是可以用 CONTAINS() 或是 FREETEXT() 兩個函式來指定,差別在哪?稍後再做解釋,我們先來看程式碼。

首先建立 OleDbConnection,連線字串固定是 provider=Search.CollatorDSO.1;EXTENDED?PROPERTIES="Application=Windows",然後建立 OleDBCommand 去執行查詢,程式碼如下:

DataTable result = new DataTable();

using (var wssConn = new OleDbConnection("provider=Search.CollatorDSO.1;EXTENDED?PROPERTIES=\"Application=Windows\""))
{
    wssConn.Open();

    var cmd = wssConn.CreateCommand();

    cmd.CommandText = @"
SELECT System.ItemName
FROM SystemIndex
WHERE SCOPE = 'D:\Search'
  AND CONTAINS('我們身邊')";

    var dataAdapter = new OleDbDataAdapter(cmd);

    dataAdapter.Fill(result);
}

CONTAINS() 跟 FREETEXT() 的差別在於 CONTAINS() 是把參數看成一個去搜尋,FREETEXT() 則是把參數看成不同去搜尋,舉例來說,我在我要索引的資料夾裡面丟了兩篇文章,這兩篇文章都有我們身邊這兩個詞。

我如果條件是下 CONTAINS('我們身邊') 只會搜尋到左邊的檔案,改下 FREETEXT('我們身邊') 條件則會兩個檔案都找出來,而且只有 CONTAINS() 支援邏輯運算,所以條件 CONTAINS('"我們" OR "身邊"') 的搜尋結果會等同於 FREETEXT('我們身邊')。

我們接著延伸問題,如果要搜尋單一個字「」這個關鍵字,我們會發現不管是下 CONTAINS('阿') 還是 FREETEXT('阿') 都搜尋不出結果,這個就跟 Windows 的分詞系統有關,中文不像英文,英文判斷符號就可以成功分開每一個字詞,但是中文的分詞不管怎麼分都無法百分之百精準,這就導致了有一些字搜不出來,不過還是有辦法可以解決的,可以改用萬用字元搜尋看看,萬用字元搜尋只有 CONTAINS() 函式支援,語法如下:

SELECT System.ItemName
FROM SystemIndex
WHERE SCOPE = 'D:\Search'
AND CONTAINS('"阿*"')

但是,這個萬用字元搜尋也不是真的萬用,星號(*)只能擺在結尾,而且它一樣是依賴 Windows Search 分詞的結果,能夠被分詞出來的詞彙才能被搜尋,像我想要用同樣的手法搜尋「」這個字,就完全搜尋不出結果來。

解決問題的第一步,永遠都是面對問題了解背後的 Context,有時候手邊現成的東西或許就能滿足使用者的需求,以上,用 Windows Search Service 做關鍵字搜尋功能就分享給大家,希望對大家有幫助。

參考資料

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學