AI 驱动的混合搜索正在封闭测试中。 加入等待列表,获取提前访问资格!

返回主页Meilisearch's logo
返回文章
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
\\ 或
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 商务入门工具构建 Shopify 店面。

Laurent Cazanove
Laurent Cazanove2024 年 8 月 19 日
Meilisearch 1.8

Meilisearch 1.8

Meilisearch 1.8 带来了否定关键词搜索,改进了搜索的稳健性和 AI 搜索,包括新的嵌入器。

Carolina Ferreira
Carolina Ferreira2024 年 5 月 7 日