如何透過篩選條件評分提升搜尋結果
為您的篩選條件分配權重,並根據文件與條件的符合程度,優先處理文件。

在本指南中,您將了解如何實作篩選條件評分功能,以增強 Meilisearch 中的搜尋功能。
什麼是篩選條件加強?
篩選條件加強,也稱為篩選條件評分,是一種進階的搜尋最佳化策略,旨在增強返回文件的相關性和精確度。這種方法不是僅僅返回符合單一篩選條件的文件,而是針對多個篩選條件使用加權系統。符合最多篩選條件的文件,或符合權重最高的篩選條件的文件,會被優先處理並在搜尋結果的頂端返回。
產生經過篩選條件加強的查詢
Meilisearch 允許使用者透過新增篩選條件來精確搜尋查詢。傳統上,搜尋結果中只會返回與這些篩選條件精確符合的文件。
透過實作篩選條件加強,您可以透過依據多個加權篩選條件的相關性對文件進行排名,來最佳化文件檢索流程。這可確保提供更量身打造且更有效的搜尋體驗。
此實作背後的想法是將權重與每個篩選條件建立關聯。值越高,篩選條件就應該越重要。在本節中,我們將示範如何實作使用這些加權篩選條件的搜尋演算法。
步驟 1 — 設定和優先處理篩選條件:權重分配
若要利用篩選條件評分功能,您需要提供篩選條件清單及其各自的權重。這有助於根據對您最重要的條件優先處理搜尋結果。
使用 JavaScript 的範例輸入
const filtersWeights = [ { filter: "genres = Animation", weight: 3 }, { filter: "genres = Family", weight: 1 }, { filter: "release_date > 1609510226", weight: 10 } ]
在上面的範例中
- 最高的權重會分配給發行日期,表示偏好 2021 年之後發行的電影
- 「動畫」類型的電影會獲得下一級的偏好
- 「家庭」類型的電影也會獲得較小的加強
步驟 2. 合併篩選條件
目標是建立所有篩選條件組合的清單,其中每個組合都會與其總權重相關聯。
以先前的範例作為參考,產生的查詢及其總權重如下所示
("genres = Animation AND genres = Family AND release_date > 1609510226", 14) ("genres = Animation AND NOT(genres = Family) AND release_date > 1609510226", 13) ("NOT(genres = Animation) AND genres = Family AND release_date > 1609510226", 11) ("NOT(genres = Animation) AND NOT(genres = Family) AND release_date > 1609510226", 10) ("genres = Animation AND genres = Family AND NOT(release_date > 1609510226)", 4) ("genres = Animation AND NOT(genres = Family) AND NOT(release_date > 1609510226)", 3) ("NOT(genres = Animation) AND genres = Family AND NOT(release_date > 1609510226)", 1) ("NOT(genres = Animation) AND NOT(genres = Family) AND NOT(release_date > 1609510226)", 0)
我們可以發現,當篩選條件符合條件 1 + 條件 2 + 條件 3 時,總權重為權重 1 + 權重 2 + 權重 3(3 + 1 + 10 = 14)。
下面,我們將說明如何建立此清單。如需自動執行此流程的詳細資訊,請參閱篩選條件組合演算法一節。
然後,您可以使用 Meilisearch 的多重搜尋 API,根據這些篩選條件執行查詢,並依據其分配的權重依降冪排序。
步驟 3. 使用 Meilisearch 的多重搜尋 API
請記得先安裝Meilisearch JavaScript 用戶端
npm install meilisearch
\\ 或
yarn add meilisearch
const { MeiliSearch } = require('meilisearch') // Or if you are in a ES environment import { MeiliSearch } from 'meilisearch' ;(async () => { // Setup Meilisearch client const client = new MeiliSearch({ host: 'https://127.0.0.1:7700', apiKey: 'apiKey', }) const INDEX = "movies" const limit = 20 const queries = [ { indexUid: INDEX, limit: limit, filter: 'genres = Animation AND genres = Family AND release_date > 1609510226' }, { indexUid: INDEX, limit: limit, filter: 'genres = Animation AND NOT(genres = Family) AND release_date > 1609510226' }, { indexUid: INDEX, limit: limit, filter: 'NOT(genres = Animation) AND genres = Family AND release_date > 1609510226' }, { indexUid: INDEX, limit: limit, filter: 'NOT(genres = Animation) AND NOT(genres = Family) AND release_date > 1609510226' }, { indexUid: INDEX, limit: limit, filter: 'genres = Animation AND genres = Family AND NOT(release_date > 1609510226)' }, { indexUid: INDEX, limit: limit, filter: 'genres = Animation AND NOT(genres = Family) AND NOT(release_date > 1609510226)' }, { indexUid: INDEX, limit: limit, filter: 'NOT(genres = Animation) AND genres = Family AND NOT(release_date > 1609510226)' }, { indexUid: INDEX, limit: limit, filter: 'NOT(genres = Animation) AND NOT(genres = Family) AND NOT(release_date > 1609510226)' } ] try { const results = await client.multiSearch({ queries }); displayResults(results); } catch (error) { console.error("Error while fetching search results:", error); } function displayResults(data) { let i = 0; console.log("=== best filter ==="); for (const resultsPerIndex of data.results) { for (const result of resultsPerIndex.hits) { if (i >= limit) { break; } console.log(`${i.toString().padStart(3, '0')}: ${result.title}`); i++; } console.log("=== changing filter ==="); } } })();
我們先匯入任務所需的程式庫。然後,我們初始化 Meilisearch 用戶端,該用戶端會連線到我們的 Meilisearch 伺服器,並定義我們要搜尋的電影索引。
接下來,我們將搜尋條件傳送至 Meilisearch 伺服器並檢索結果。multiSearch
函數可讓我們一次傳送多個搜尋查詢,這會比逐一傳送更有效率。
最後,我們以格式化的方式印出結果。外部迴圈會迭代每個篩選條件的結果。內部迴圈會迭代指定篩選條件的命中(實際搜尋結果)。我們會列印出每個電影標題,並加上數字前綴。
我們會得到下列輸出
=== best filter === 000: Blazing Samurai 001: Minions: The Rise of Gru 002: Sing 2 003: The Boss Baby: Family Business === changing filter === 004: Evangelion: 3.0+1.0 Thrice Upon a Time 005: Vivo === changing filter === 006: Space Jam: A New Legacy 007: Jungle Cruise === changing filter === 008: Avatar 2 009: The Flash 010: Uncharted ... === changing filter ===
篩選條件組合演算法
雖然手動篩選方法可提供精確的結果,但這不是最有效率的方法。自動化此流程將大幅提升速度和效率。讓我們建立一個函數,該函數會將查詢參數和加權篩選條件清單作為輸入,並輸出搜尋命中清單。
實用函數:篩選條件操作的建構區塊
在深入探討核心函數之前,必須建立一些實用函數來處理篩選條件操作。
否定篩選條件
negateFilter
函數會傳回指定篩選條件的相反條件。例如,如果提供 genres = Animation
,它會傳回 NOT(genres = Animation)
。
function negateFilter(filter) { return `NOT(${filter})`; }
彙總篩選條件
aggregateFilters
函數會使用「AND」運算合併兩個篩選條件字串。例如,如果提供 genres = Animation
和 release_date > 1609510226
,它會傳回 (genres = Animation) AND (release_date > 1609510226)
。
function aggregateFilters(left, right) { if (left === "") { return right; } if (right === "") { return left; } return `(${left}) AND (${right})`; }
產生組合
getCombinations
函數會從輸入陣列產生指定大小的所有可能組合。這對於根據分配的權重建立不同的篩選條件組合集至關重要。
function getCombinations(array, size) { const result = []; function generateCombination(prefix, remaining, size) { if (size === 0) { result.push(prefix); return; } for (let i = 0; i < remaining.length; i++) { const newPrefix = prefix.concat([remaining[i]]); const newRemaining = remaining.slice(i + 1); generateCombination(newPrefix, newRemaining, size - 1); } } generateCombination([], array, size); return result; }
核心函數:boostFilter
現在我們有了實用函數,我們可以繼續根據其分配的權重,以更動態的方式產生篩選條件組合。這是使用 boostFilter
函數完成的,它會根據各自的權重合併和排序篩選條件。
function boostFilter(filterWeights) { const totalWeight = filterWeights.reduce((sum, { weight }) => sum + weight, 0); const weightScores = {}; const indexes = filterWeights.map((_, idx) => idx); for (let i = 1; i <= filterWeights.length; i++) { const combinations = getCombinations(indexes, i); for (const filterIndexes of combinations) { const combinationWeight = filterIndexes.reduce((sum, idx) => sum + filterWeights[idx].weight, 0); weightScores[filterIndexes] = combinationWeight / totalWeight; } } const filterScores = []; for (const [filterIndexes, score] of Object.entries(weightScores)) { let aggregatedFilter = ""; const indexesArray = filterIndexes.split(",").map(idx => parseInt(idx)); for (let i = 0; i < filterWeights.length; i++) { if (indexesArray.includes(i)) { aggregatedFilter = aggregateFilters(aggregatedFilter, filterWeights[i].filter); } else { aggregatedFilter = aggregateFilters(aggregatedFilter, negateFilter(filterWeights[i].filter)); } } filterScores.push([aggregatedFilter, score]); } filterScores.sort((a, b) => b[1] - a[1]); return filterScores; }
分解 boostFilter 函數
讓我們剖析此函數,以更好地了解其組件和運作方式。
1. 計算總權重
函數開始時會計算 totalWeight
,這只是 filterWeights
陣列中所有權重的總和。
const totalWeight = filterWeights.reduce((sum, { weight }) => sum + weight, 0);
2. 建立權重和索引結構
此處會初始化兩個重要的結構
weightScores
:保留篩選條件的組合及其相關的相對分數indexes
:將每個篩選條件對應到原始 filterWeights 陣列中位置的陣列
const weightScores = {}; const indexes = filterWeights.map((_, idx) => idx);
3. 計算加權篩選條件組合
針對每個組合,我們會計算其權重,並將其相對分數儲存在 weightScores
物件中。
for (let i = 1; i <= filterWeights.length; i++) { const combinations = getCombinations(indexes, i); for (const filterIndexes of combinations) { const combinationWeight = filterIndexes.reduce((sum, idx) => sum + filterWeights[idx].weight, 0); weightScores[filterIndexes] = combinationWeight / totalWeight; } }
4. 彙總和否定篩選條件
在這裡,我們形成彙總的篩選條件字串。weightScores
中的每個組合都會經過處理,並填入 filterScores
清單中,同時填入其相對分數。
const filterScores = []; for (const [filterIndexes, score] of Object.entries(weightScores)) { let aggregatedFilter = ""; const indexesArray = filterIndexes.split(",").map(idx => parseInt(idx)); for (let i = 0; i < filterWeights.length; i++) { if (indexesArray.includes(i)) { aggregatedFilter = aggregateFilters(aggregatedFilter, filterWeights[i].filter); } else { aggregatedFilter = aggregateFilters(aggregatedFilter, negateFilter(filterWeights[i].filter)); } } filterScores.push([aggregatedFilter, score]); }
5. 排序並傳回篩選條件分數
最後,filterScores
清單會根據分數依降冪排序。這可確保最「重要」的篩選條件(由權重決定)位於開頭。
filterScores.sort((a, b) => b[1] - a[1]); return filterScores;
使用篩選條件加強函數
現在我們有了 boostFilter
函數,我們可以示範其在範例中的功效。此函數會傳回陣列的陣列,其中每個內部陣列都包含
- 根據輸入條件合併的篩選條件
- 表示篩選條件加權重要性的分數
當我們將函數套用到範例時
boostFilter([["genres = Animation", 3], ["genres = Family", 1], ["release_date > 1609510226", 10]])
我們會收到下列輸出
[ [ '((genres = Animation) AND (genres = Family)) AND (release_date > 1609510226)', 1 ], [ '((genres = Animation) AND (NOT(genres = Family))) AND (release_date > 1609510226)', 0.9285714285714286 ], [ '((NOT(genres = Animation)) AND (genres = Family)) AND (release_date > 1609510226)', 0.7857142857142857 ], [ '((NOT(genres = Animation)) AND (NOT(genres = Family))) AND (release_date > 1609510226)', 0.7142857142857143 ], [ '((genres = Animation) AND (genres = Family)) AND (NOT(release_date > 1609510226))', 0.2857142857142857 ], [ '((genres = Animation) AND (NOT(genres = Family))) AND (NOT(release_date > 1609510226))', 0.21428571428571427 ], [ '((NOT(genres = Animation)) AND (genres = Family)) AND (NOT(release_date > 1609510226))', 0.07142857142857142 ] ]
從經過加強的篩選條件產生搜尋查詢
現在我們有了來自 boostFilter
函數的篩選條件優先處理清單,我們可以使用它來產生搜尋查詢。讓我們建立一個 searchBoostFilter
函數,以根據經過加強的篩選條件自動產生搜尋查詢,並使用提供的 Meilisearch 用戶端執行搜尋查詢。
async function searchBoostFilter(client, filterScores, indexUid, q) { const searchQueries = filterScores.map(([filter, _]) => { const query = { ...q }; query.indexUid = indexUid; query.filter = filter; return query; }); const results = await client.multiSearch({ queries: searchQueries }); return results; }
此函數會採用下列參數
client
:Meilisearch 用戶端執行個體。filterScores
:篩選條件及其對應分數的陣列。indexUid
:您要在其中搜尋的索引q
:基本查詢參數
針對 filterScores
中的每個篩選條件,我們會
- 使用展開運算符建立基本查詢參數
q
的複本 - 更新目前搜尋查詢的
indexUid
和filter
值 - 將修改後的
query
新增至我們的searchQueries
陣列
然後,函數會傳回多重搜尋路由的原始結果。
範例:使用篩選條件分數擷取最熱門的電影
讓我們建立一個函數,以顯示符合我們定義的搜尋限制,並根據我們的優先篩選條件(bestMoviesFromFilters
函數)顯示最熱門的電影標題。
async function bestMoviesFromFilters(client, filterWeights, indexUid, q) { const filterScores = boostFilter(filterWeights); const results = await searchBoostFilter(client, filterScores, indexUid, q); const limit = results.results[0].limit; let hitIndex = 0; let filterIndex = 0; for (const resultsPerIndex of results.results) { if (hitIndex >= limit) { break; } const [filter, score] = filterScores[filterIndex]; console.log(`=== filter '${filter}' | score = ${score} ===`); for (const result of resultsPerIndex.hits) { if (hitIndex >= limit) { break; } console.log(`${String(hitIndex).padStart(3, '0')}: ${result.title}`); hitIndex++; } filterIndex++; } }
此函數會使用 boostFilter
函數來取得篩選條件組合及其分數的清單。
然後,searchBoostFilter
函數會取得所提供篩選條件的結果。
它也根據我們基本查詢中設定的限制,決定我們希望顯示的最大電影標題數量。
此函數使用迴圈,遍歷結果。
- 如果目前顯示的電影標題計數 (
hitIndex
) 達到指定的limit
,函數將停止處理後續內容。 - 對於多重搜尋查詢的每一組結果,函數會顯示套用的篩選條件及其分數。
- 然後,它會遍歷搜尋結果(或命中),並顯示電影標題,直到達到
limit
或顯示目前篩選器的所有結果。 - 對於具有不同篩選條件組合的下一組結果,此過程會繼續進行,直到達到整體
limit
或顯示所有結果。
讓我們在範例中使用我們的新函數。
bestMoviesFromFilters(client, [ { filter: "genres = Animation", weight: 3 }, { filter: "genres = Family", weight: 1 }, { filter: "release_date > 1609510226", weight: 10 } ], "movies", { q: "Samurai", limit: 100 } )
我們會得到下列輸出
=== filter '((genres = Animation) AND (genres = Family)) AND (release_date > 1609510226)' | score = 1.0 === 000: Blazing Samurai === filter '((genres = Animation) AND (NOT(genres = Family))) AND (release_date > 1609510226)' | score = 0.9285714285714286 === === filter '((NOT(genres = Animation)) AND (genres = Family)) AND (release_date > 1609510226)' | score = 0.7857142857142857 === === filter '((NOT(genres = Animation)) AND (NOT(genres = Family))) AND (release_date > 1609510226)' | score = 0.7142857142857143 === === filter '((genres = Animation) AND (genres = Family)) AND (NOT(release_date > 1609510226))' | score = 0.2857142857142857 === 001: Scooby-Doo! and the Samurai Sword 002: Kubo and the Two Strings === filter '((genres = Animation) AND (NOT(genres = Family))) AND (NOT(release_date > 1609510226))' | score = 0.21428571428571427 === 003: Samurai Jack: The Premiere Movie 004: Afro Samurai: Resurrection 005: Program 006: Lupin the Third: Goemon's Blood Spray 007: Hellboy Animated: Sword of Storms 008: Gintama: The Movie 009: Heaven's Lost Property the Movie: The Angeloid of Clockwork 010: Heaven's Lost Property Final – The Movie: Eternally My Master === filter '((NOT(genres = Animation)) AND (genres = Family)) AND (NOT(release_date > 1609510226))' | score = 0.07142857142857142 === 011: Teenage Mutant Ninja Turtles III
結論
在本指南中,我們逐步介紹了實作評分篩選功能的過程。我們學習了如何設定加權篩選器並自動產生篩選條件組合,然後根據其權重對這些組合進行評分。接著,我們探索了如何藉助 Meilisearch 的多重搜尋 API 使用這些加強篩選器建立搜尋查詢。
我們計劃在 Meilisearch 引擎中整合評分篩選器。請在先前的連結中提供您的意見,以協助我們決定優先順序。
如需更多關於 Meilisearch 的資訊,您可以訂閱我們的電子報。您可以查看路線圖並參與我們的產品討論,以進一步了解我們的產品。
如有其他問題,請加入我們在 Discord 上的開發人員社群。