AI 驅動的混合搜尋功能正在封閉測試中。 加入候補名單 以搶先體驗!

前往首頁Meilisearch 的標誌
返回文章
2023 年 9 月 7 日

如何透過篩選條件評分提升搜尋結果

為您的篩選條件分配權重,並根據文件與條件的符合程度,優先處理文件。

Carolina Ferreira
Carolina Ferreira開發者倡議者 @ Meilisearch@CarolainFG
How to boost your search results with filter scoring

在本指南中,您將了解如何實作篩選條件評分功能,以增強 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 = Animationrelease_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 的複本
  • 更新目前搜尋查詢的 indexUidfilter
  • 將修改後的 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 上的開發人員社群。

How to add AI-powered search to a React app

如何將 AI 驅動的搜尋新增至 React 應用程式

使用 Meilisearch 的 AI 驅動搜尋建立 React 電影搜尋和推薦應用程式。

Carolina Ferreira
Carolina Ferreira2024 年 9 月 24 日
Build your Next.js Shopify storefront with Blazity

使用 Blazity 建立您的 Next.js Shopify 店面

學習使用 Next.js 和 Blazity commerce starter 建立 Shopify 店面。

Laurent Cazanove
Laurent Cazanove2024 年 8 月 19 日
Meilisearch 1.8

Meilisearch 1.8

Meilisearch 1.8 帶來了負關鍵字搜尋、搜尋強健性和 AI 搜尋的改進,包括新的嵌入器。

Carolina Ferreira
Carolina Ferreira2024 年 5 月 7 日