前往主页Meilisearch 的标志
返回文章
2023 年 9 月 7 日

如何通过过滤得分提升搜索结果

为您的过滤器分配权重,并根据它们与您的标准匹配的程度来优先处理文档。

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 时,总权重为 weight1 + weight2 + weight3 (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: 'http://localhost: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
Carolina Ferreira2024 年 9 月 24 日
Meilisearch is too slow

Meilisearch 太慢了

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

Clément Renault
Clément Renault2024 年 8 月 20 日