想要更好地控制您的搜索设置?了解我们的灵活的基础设施定价

回到主页Meilisearch 的标志
返回文章

如何通过筛选器评分提升搜索结果

为您的筛选器分配权重,并根据它们与您的条件的匹配程度来确定文档的优先级。

2023 年 9 月 7 日阅读 6 分钟
Carolina Ferreira
Carolina FerreiraMeilisearch 开发者宣传员@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  
\\\\ or  
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://: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

Meilisearch indexes embeddings 7x faster with binary quantization

Meilisearch 使用二值量化将嵌入索引速度提高7倍

通过在向量存储 Arroy 中实现二值量化,显著减少了大型嵌入的磁盘空间使用和索引时间,同时保持了搜索相关性和效率。

Tamo
Tamo2024年11月29日
How to add AI-powered search to a React app

如何向 React 应用添加 AI 驱动的搜索

使用 Meilisearch 的 AI 驱动搜索构建 React 电影搜索和推荐应用。

Carolina Ferreira
卡罗莱纳·费雷拉2024年9月24日
Meilisearch is too slow

Meilisearch 太慢了

在这篇博文中,我们探讨了 Meilisearch 文档索引器所需的增强功能。我们将讨论当前的索引引擎、其缺点以及优化性能的新技术。

Clément Renault
克莱门特·雷诺2024年8月20日
© . This site is unofficial and not affiliated with Meilisearch.