image

从零构建搜索引擎 build demo search engine from scratch

image

我们每天都会使用搜索引擎:打开google等搜索引擎,输入关键词,检索出结果,这是一次搜索;当打开历史记录旁边的🔍按钮,输入关键词,检索出结果,这也是一次搜索。
本文将从原理开始到最终实现一个搜索demo,探究搜索引擎背后是如何实现“一瞬间”从海量数据中检索出想要的结果的。

配合本文,实现了一个本文讲述的搜索引擎的demo用于检索内容,支持AND、包含、排除查询等常见的查询,也附带上简单的benchmark对比,可见我的github仓库:https://github.com/578223592/go-small-practices/tree/main/search_engine

全文搜索

全文搜索可以简单的定义为:根据输入的查询语句(query),在一组文档(document)中,筛选出符合条件的文档。

过程

要实现全文搜索,我们可以想象到最简单的方式就是循环去匹配,like如下的伪代码,想象一下这种循环会在文档数量增加和查询复杂后产生多少次循环匹配计算:

    a = ["just", "do"] #查询条件count = 0  #匹配数量for d in tokenized_documents:  #遍历文档for t in d:  #遍历文档中的每个语句for c in a: # 遍历查询条件if c == t: # 如果匹配,则count++count += 1return count

因此在现实世界,会对文档进行预处理(分析),分析之后产生索引。查询的时候根据索引去匹配定位到源文件,比如下图这样的管道:

image

实际上索引是不同查询引擎里面影响性能的关键,我们通常可以用如下指标来评价一个搜索索引的好坏:

  • 计算索引的速度
  • 压缩率
  • 可拓展性
  • 查询性能

由于本人目前对搜索索引的了解也不是特别深入,因此这里就不卖弄了。

关键词

document​:一组单词。

Do it. Just do it. Don’t let your dreams be dreams. Yesterday, you said tomorrow. So just do it. Make your dreams come true. Just do it. Some people dream of success, while you’re gonna wake up and work hard at it. Nothing is impossible. You should get to the point where anyone else would quit, and you’re not gonna stop there. No, what are you waiting for? Do it! Just do it! Yes you can. Just do it.

term​:词。

just

token​:用户输入的查询词(或其变体)在被检索文档中出现的每一个具体匹配实例。

Just do it. .... So just do it. Make your dreams come true. Just do it. ... Just do .... Just do it.

query​:指定特定格式的查询语句

以下举例的是本文构建出来的搜索引擎支持的查询格式,不同引擎的语句可能是不同的。

just:查询包含just的文档

just do:查询包含just或者包含do的文档

just AND do:查询同时包含just和do的文档

"just do":查询顺序出现just和do的文档

just do+it:出现it的文档中,查询包含just或者包含do的文档

just do -it:查询包含just或者包含do的文档,再剔除掉包含it的文档。

score​:文档匹配的分数,如果多个文档都符合查询条件,那么文档的列表会按照分数从高到低给出。本文搜索引擎是按照文档中匹配到的词的个数来评分,现实世界中的评分会参考更多因素, 比如说地区、性别等等。

index​:索引,搜索的索引为inverted-index​(倒序索引),其可以根据关键词寻找关键词出现的位置,通常的结构如下。这是搜索引擎如此之快的关键奥秘!通过inverted-index​,我们寻找关键词不再需要通过遍历文档,而是可以直接通过类似key->value查找的方式,速度非常快。

just -> [(1, 0), (1, 5), (1, 20), (1, 28), (1, 76), (1, 82)] #含义是just出现的位置是document1的第0个位置,document1的第5个位置,document1的第20个位置。。。
do -> [(0, 94), (0, 399), (0, 455), (0, 493), (1, 1), (1, 3), (1, 6), (1, 21), (1, 29), (1, 74), (1, 77), (1, 83), (2, 15), (2, 33), (2, 44)] #含义是do出现的位置是。。。

搜索索引称为inverted-index​的原因是通常的索引是根据位置去找内容,而inverted-index是根据内容去找位置,是inverted版本的普通索引。

关键算法实现

创建倒序索引:

func (a *Analyser) AnalyseAndIndex(documents []string) {for docId, oneDocument := range documents {tokens := regexp.MustCompile(`\w+`).FindAllString(oneDocument, -1) // 使用正则将文档拆解成termfor index, token := range tokens {token = strings.ToLower(token)local_ := local{DocId: int64(docId), Indexes: int64(index)} //use inverted index to index termsif a.index == nil {a.index = make(map[string][]local)}a.index[token] = append(a.index[token], local_)}}
}

搜索:

我们将文档检索分成四种模式,如下代码块所示,默认来说是OR模式,如果出现其他模式的关键词,那么就会调整对应的搜索模式:

const (AND     searchModeExp = "AND"OR      searchModeExp = "OR"INCLUDE searchModeExp = "+"EXCLUDE searchModeExp = "-"
)

我们创建了两个结果数组,分别用于记录结果和记录必须要排除掉的文档(EXCLUDE searchModeExp = "-"​):

includeDocScoresMap := make(map[int64]float64)excludeDocMap := make(map[int64]any)

此外,还需要注意的是,为了支持"just do"​这样保证顺序的查找,我们需要匹配成对出现的"​,然后根据其中的内容去文档中固定匹配内容,而不能直接简单组合不同文档的内容。

整个搜索过程关键算法如下:

func (a *Analyser) Search(keyWords string) []SearchRes {includeDocScoresMap := make(map[int64]float64)excludeDocMap := make(map[int64]any)queryExpressions := a.parseQuery(keyWords)var searchMode = OR        // for i := 0; i < len(queryExpressions); i++ {var locals []localif isSearchMode(queryExpressions[i]) { //调整搜索模式searchMode = searchModeExp(queryExpressions[i])continue} else if queryExpressions[i] == Quote {endIndex := stringsIndex(queryExpressions[i+1:], Quote)if endIndex == -1 {continue // 缺少结束引号,忽略}if endIndex == 0 {continue // 引号中间没有任何exp,忽略}quoteQueryExpressions := queryExpressions[i+1 : i+1+endIndex]i += endIndex + 1for i2 := range quoteQueryExpressions {quoteQueryExpressions[i2] = strings.ToLower(quoteQueryExpressions[i2])}firstExp, otherExp := quoteQueryExpressions[0], quoteQueryExpressions[1:]var ok boollocals, ok = a.index[firstExp]if !ok {// not found,early continuecontinue}for _, exp := range otherExp {locals = a.findExpInIndex(locals, exp)}} else {//	searchlocals = a.index[queryExpressions[i]]}switch searchMode {case AND:for _, l := range locals {if _, ok := includeDocScoresMap[l.DocId]; ok {includeDocScoresMap[l.DocId] += 1}}case OR:for _, l := range locals {includeDocScoresMap[l.DocId] += 1}case INCLUDE:locsMap := make(map[int64]float64, len(includeDocScoresMap))for _, l := range locals {locsMap[l.DocId] = includeDocScoresMap[l.DocId] + 1}includeDocScoresMap = locsMapcase EXCLUDE:for _, l := range locals {excludeDocMap[l.DocId] = nil}}searchMode = OR}res := make([]SearchRes, 0)for k, v := range includeDocScoresMap {if _, ok := excludeDocMap[k]; ok {continue}res = append(res, SearchRes{DocId: k, Score: v})}slices.SortFunc(res, func(a, b SearchRes) int {return int(b.Score - a.Score) // order by score desc})return res
}

真正的搜索引擎考虑更多内容,比如说搜索计划、ast、黑客、错误纠正、类型匹配(比如说搜索攀枝花,推荐攀枝花城市、攀枝花花朵等)。

总结

本文通过讲解搜索中analyze-》index—》query步骤,其中出现的关键名词,和一个简单的demo代码,讲解了搜索引擎实现的关键技术, 希望对你有所帮助!

参考:

  • strong thanks :Building a search engine from scratch | by Halvor Fladsrud Bø | Medium

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/914485.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/914485.shtml
英文地址,请注明出处:http://en.pswp.cn/news/914485.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

pytorch小记(二十九):深入解析 PyTorch 中的 `torch.clip`(及其别名 `torch.clamp`)

pytorch小记&#xff08;二十九&#xff09;&#xff1a;深入解析 PyTorch 中的 torch.clip&#xff08;及其别名 torch.clamp&#xff09;深入解析 PyTorch 中的 torch.clip&#xff08;及其别名 torch.clamp&#xff09;一、函数签名二、简单示例三、广播支持四、与 Autograd…

快速分页wpf

/*没有在xaml设置上下文window.context是因为 命名空间一直对应不上 所以在xaml.cs 里面绑定*/ <Window x:Class"DataGrid.views.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft…

如何彻底禁用 Chrome 自动更新

如何彻底禁用 Chrome 自动更新 随着谷歌将 Chrome 浏览器版本升级至 138&#xff0c;它即将彻底抛弃对 Manifest V2 扩展的支持。许多用户希望将浏览器版本锁定在 138&#xff0c;以继续使用 uBlock Origin、Tampermonkey 等常用扩展。 本文总结了四种有效方法&#xff0c;帮助…

流批一体的“奥卡姆剃刀”:Apache Cloudberry 增量物化视图应用解析

引言&#xff1a;流批一体&#xff0c;理想与现实的鸿沟 在数据驱动的今天&#xff0c;“实时”二字仿佛拥有魔力&#xff0c;驱使着无数企业投身于流批一体架构的建设浪潮中。我们渴望实时洞察业务变化&#xff0c;实时响应用户需求。以 Apache Flink 为代表的流处理引擎&…

C# 入门教程(三):详解字段、属性、索引器及各类参数与扩展方法

文章目录一、字段、属性、索引器、常量1.字段2.属性2.1 什么是属性2.2 属性的声明2.3 属性与字段的关系3 索引器4. 常量二、传值 输出 引用 数组 具名 可选参数&#xff0c;扩展方法2.1 传值参数2.1.1 值类型 传参2.1.2 引用类型 传参2.2 引用参数2.2.1 引用参数-值类型 传参2.…

《美术教育研究》是什么级别的期刊?是正规期刊吗?能评职称吗?

​问题解答&#xff1a;问&#xff1a;《美术教育研究》是不是核心期刊&#xff1f;答&#xff1a;不是&#xff0c;是知网收录的第一批认定学术期刊。问&#xff1a;《美术教育研究》级别&#xff1f;答&#xff1a;省级。主管单位&#xff1a; 安徽出版集团有限责任公司 主办…

每日算法刷题Day47:7.13:leetcode 复习完滑动窗口一章,用时2h30min

思考: 遇到子数组/子字符串可以考虑能不能用滑动窗口&#xff0c; 定长:逆向思维,答案不定 最大长度/最小长度:一般求长度 越长越合法/越短越合法/恰好:一般求数量 主要思考窗口条件成立&#xff0c; 判断条件是符合窗口条件(最小长度/越长越合法还是不符合(最大长度/越短越合法…

电流驱动和电压驱动的区别

理解电流驱动和电压驱动的区别对电路设计至关重要&#xff0c;尤其在高速、高抗噪要求的场景&#xff08;如LVDS&#xff09;。以下是两者的核心对比&#xff1a;一、电压驱动 (Voltage Drive) 核心原理&#xff1a; 驱动器输出一个受控的电压&#xff08;与负载阻抗无关&#…

宿舍电费查询——以ZUA为例

宿舍电费查询——以ZUA为例0. 安装抓包环境手机端桌面端1. 登录1.1 开启抓包后进入缴费页面&#xff1a;1.2 分析请求1.3 编写登录代码2. 获取楼栋及房间ID2.1 获取楼栋ID2.2 编写获取楼栋ID代码2.3 获取房间ID2.4 编写获取房间ID代码3. 获取剩余电费&#xff1a;3.1 选择房间号…

vue中计算属性的介绍

Vue.js 中的计算属性是基于它的响应式系统来实现的&#xff0c;它可以根据 Vue 实例的数据状态来动态计算出新的属性值。在 Vue 组件中&#xff0c;计算属性常用于对数据进行处理和转换&#xff0c;以及动态生成一些需要的数据。一、使用方式1.定义计算属性&#xff1a; 在Vue组…

MFC UI控件CheckBox从专家到小白

文章目录CheckBox勾选框控件控件与变量绑定控件点击消息映射互斥CheckBox勾选框控件 控件与变量绑定 方案一&#xff1a; BOOL m_bEnable1; BOOL m_bEnable2; void A::DoDataExchange(CDataExchange* pDX) {DDX_Check(pDX, IDC_CK_1, m_bEnable1);DDX_Check(pDX, IDC_CK_2, …

阿尔卡特ACT 250 ATP 150 AND ATP 400 分子泵控制器TURBOMOLECULAR PUMP CONTROLLER ALCATEL

阿尔卡特ACT 250 ATP 150 AND ATP 400 分子泵控制器TURBOMOLECULAR PUMP CONTROLLER ALCATEL

python的小学课外综合管理系统

前端开发框架:vue.js 数据库 mysql 版本不限 后端语言框架支持&#xff1a; 1 java(SSM/springboot)-idea/eclipse 2.NodejsVue.js -vscode 3.python(flask/django)–pycharm/vscode 4.php(thinkphp/laravel)-hbuilderx 数据库工具&#xff1a;Navicat/SQLyog等都可以 摘要 随着…

实用技巧 Excel 与 XML互转

一 概述 在android多语言适配中&#xff0c;可能提供的是excel格式的多语言翻译&#xff0c;而且翻译数量非常庞大。那手动一个一个往xml里面添加效率非常低&#xff0c;这时候就需要把excel快速转为android可以直接用的资源文件string.xml二 转换流程2.1 第一步任意文件夹或者…

云原生技术与应用-Containerd容器技术详解

目录 一.Containerd概述 1.什么是containerd 2.Containerd的起源与背景 二.Containerd架构 1.Containerd架构概述 2.核心组件解析 三.安装配置Containerd 1.安装Containerd 2.配置Containerd 四.Containerd基本操作 1.镜像类操作 2.容器类操作 3.任务类操作 4.其他操作 一.…

LINUX714 自动挂载/nfs;物理卷

开机自动挂载 /etc/fstab vim /etc/fstab /dev/sdb2 /u2 ext4 defaults 0 0 mount -a [rootweb ~]# vim /etc/fstab [rootweb ~]# cat /etc/fstab# # /etc/fstab # Created by anaconda on Sat Apr 19 17:11:28 2025 # # Accessible filesystems, by reference, are maintai…

系统性学习C语言-第十六讲-深入理解指针(6)

系统性学习C语言-第十六讲-深入理解指针&#xff08;6&#xff09;1. sizeof 和 strlen 的对比1.1 sizeof 1.2 strlen 1.3 sizeof 和 strlen 的对比2. 数组和指针笔试题解析2.1 一维数组2.2 字符数组2.3 二维数组3. 指针运算笔试题解析3.1 题目1&#xff1a;3.2 题目…

8:从USB摄像头把声音拿出来--ALSA大佬登场!

前言前面的章节我们从认识摄像头开始&#xff0c;逐渐认识的YCbCr&#xff0c;并对其进行了H264的编码以及MP4封装。整个过程中&#xff0c;我们大致使用了V4L2和FFmpeg这两个重量级工具&#xff0c;就像我们前面章节所讲&#xff0c;V4L2只是给图像做服务的&#xff0c;并不参…

Linux 命令:useradd

Linux useradd 命令详细教程 useradd 是 Linux 系统中用于创建新用户账户的基础命令&#xff0c;它通过配置文件&#xff08;如 /etc/passwd、/etc/shadow&#xff09;和默认设置自动完成用户创建流程。本文将详细介绍其用法、参数及相关配置。资料已经分类整理好&#xff1a;h…

Pytest之收集用例规则与运行指定用例

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 小伙伴们大家好呀&#xff0c;今天笔者会给大家讲解一下pytest是如何收集我们写好的用例&#xff1f;我们又有哪些方式来运行单个用例或者批量运行用例呢&#xff…