【Go语言开发】简单了解一下搜索引擎并用go写一个demo

news2024/11/23 7:34:26

写在前面

这篇文章我们一起来了解一下搜索引擎的原理,以及用go写一个小demo来体验一下搜索引擎。

简介

搜索引擎一般简化为三个步骤

  • 爬虫:爬取数据源,用做搜索数据支持。
  • 索引:根据爬虫爬取到的数据进行索引的建立。
  • 排序:对搜索的结果进行排序。

然后我们再对几个专业名词做一个简单解释

  • document:用于构建索引库的数据
  • term:将一段文本进行分词,分词之后的每个最小单元叫做 Term,比如“苹果发布会”,分词之后就是【苹果,发布会】,“苹果”和“发布会”就是最小单元的 term。
  • token:token 是 term 的一次出现,它包含 term 文本和相应的起止偏移,以及一个类型字符串。一句话中可以出现多次相同的词语,它们都用同一个 term 表示,但是用不同的 Token,每个 Token 标记该词语出现的地方。比如 token中不仅有term还有这个term在这个文档的位置。

1. 爬虫

爬虫就很简单了,不是重点,我们准备好数据源即可。
在这里插入图片描述

2. 索引

2.1 正排索引&倒排索引

索引分成正排索引和倒排索引。
正排索引:将文档按照文档顺序进行组织的索引结构。
倒排索引:根据词条来组织文档数据的索引结构。

举个例子:
假设我们有三个文档:

文档1,内容为"postman datagrip goland";
文档2,内容为"goland vscode";
文档3,内容为"pycharm goland"。

使用正排索引和倒排索引来存储这些文档:

  • 正排索引:

文档1:“postman datagrip goland”
文档2:“goland vscode”
文档3:“pycharm goland”

在正排索引中,我们按照文档的顺序存储了每个文档的完整内容。

  • 倒排索引:

postman:文档1,
datagrip:文档1,
goland:文档1,文档2,文档3
vscode:文档2
pycharm:文档3

在倒排索引中,我们将每个词条映射到包含该词条的文档集合上。例如,"postman"出现在文档1中

2.2 构建索引

  • 读取数据源

首先观察数据源,确定了我们的索引对象是第16个,也就是电影的主要内容。

读取数据源,构造索引数据

func fileOpen() []string {
    file, err := os.Open("movies.csv")
     if err != nil {
        fmt.Println("err", err)
    }
    defer file.Close()
     // 创建一个 Scanner 用来读取文件内容
    docx := make([]string, 0)
    scanner := bufio.NewScanner(file)
     // 逐行读取文件内容并打印
     for scanner.Scan() {
       re := make([]string, 0)
       line := scanner.Text()
        re = strings.Split(line, ",")
        docx = append(docx, re[16])
     }
   docx = docx[1:]

   return docx
}
  • 分词

当我们读取数据之后,要对数据进行分词,分成一个个的词,用作建立索引库。
但分词之前我们要先数据清洗一下,比如中文就是去掉一些语气词,标点符号;英文则是去除一些语气词,做预干转移(过去式,未来式都变成现在式,比如learning --> learn),转成小写之类的。
先定义一下StopWord,如果出现StopWord里面的词,就进行删除替换。

var StopWord = []string{",", ".", "。", "*", "(", ")", "'", "\""}

定义一个 removeShopWord 的 func,传入 word,也就是段落,先进行数据的清洗,再将word进行分词。

func removeShopWord(word string) string {
     for i := range StopWord {
        word = strings.Replace(word, StopWord[i], "", -1)
     }

    return word
}

定义一个 tokenize 进行分词操作。使用 github.com/go-ego/gse 包进行分词操作。

var gobalGse gse.Segmenter

func InitConfig() {
    gobalGse, _ = gse.New()
 }

func tokenize(text string) []string {
   text = removeShopWord(text)

   return gobalGse.CutSearch(text)
 }

3. 索引构建

定义一个map结构,key 是一个 term,value 是包含有 term 关键字的文档的 id 数组。

type InvertedIndex map[string][]int

构建索引

func BuildIndex(docx []string) InvertedIndex {
   index := make(InvertedIndex)
     for i, d := range docx { // 遍历所有的docx
       for _, word := range tokenize(d) { // 对所有的docx进行token
           if _, ok := index[word]; !ok { // 如果index不存在这个term了
              index[word] = []int{i} // 初始化并放入 行数
          } else {
             index[word] = append(index[word], i) 
             // 如果index不存在,则放入该term所在的 行数,也就是 行数
          }
        }
     }
 
     return index
}

这里我们的 token 和 term 是一样的了,因为token中只有term,没有定义别的东西,比如term在doc的位置等等…

3.1 搜索 排序

  • 搜索

我们定义 query 为搜索的内容,对query进行分词操作,然后再存储符合要求的docx文档的id。
那我们的search函数的传入就是 倒排索引 index,搜索词条 query,正排索引 docs

func search(index InvertedIndex, query string, docs []string) ([]string, []string) {
    result := make(map[int]bool)
     qy := tokenize(query) // query词条进行分词
    for _, word := range qy { // 遍历分完词的每一个term
        if doc, ok := index[word]; ok { 
        // 搜索倒排索引中,term对应的doc数组,doc数组就是存在该term词条的所有的doc id
         for _, d := range doc { 
          // 对doc数组进行遍历,获取所有的doc id,并且进行标志。
              result[d] = true 
           }
       }
    }

    output := []string{}
     ford := range result {
        output = append(output, docs[d]) 
        // 利用正排索引,找到id对应的存储内容并返回
     }
     return output, qy
}

3.2 排序

当我们搜索完结果后,自然会有结果,但是这些结果的排序是不合理的,我们要进行重新排序。排序的规则也有很多,比如文档相似度,竞价排名等等…

那么我们这里就用 最简单的TFIDF来进行计算所搜索出来的doc和term之间的权重。

首先了解一下TFIDF:
TF(词频)指的是某个词在文档中出现的频率。在计算TF时,我们可以简单地使用词出现的次数除以文档中的总词数。
IDF(逆文档频率)指的是某个词在文档集合中的多少文档中出现过的程度。计算IDF时,我们可以将所有文档数目除以包含该词的文档数目。
TF-IDF的计算方式是将TF和IDF相乘,得到一个词在文档中的重要性分数。这个分数能够衡量一个词对于文档的重要性:如果一个词在某个文档中频繁出现,并且在整个文档集合中罕见,那么它可能是一个具有较高重要性的词。

计算TF:
term在这个文档中的出现的次数/这个document所有的分词的数量

func calculateTF(term string, document string) float64 {
    termCount := strings.Count(document, term)
    totalWords := len(tokenize(document))
    return float64(termCount) / float64(totalWords)
}

计算IDF:
所有文档的数量/term在所有文档中出现的次数

func calculateIDF(term string, documents []string) float64 {
    docWithTerm := 0
    for _, doc := range documents {
       if strings.Contains(doc, term) {
          docWithTerm++
       }
    }
    return float64(len(documents)) / float64(docWithTerm)
}

TF*IDF即可获取权重,下面这里是由于数据问题,我是乘以100的

func calculateTFIDF(term string, document string, documents []string) float64 {
    tf := calculateTF(term, document)
    idf := calculateIDF(term, documents)
    return tf * idf * 100.0
}

具体排序:
qy为输入的query分词后的token形式,res则是搜索结构,返回值是将res排序好的结果。

func sortRes(qy []string, res []string) []*SortRes {
   exist := make(map[int]*SortRes)
     for _, v := range qy {// 遍历每一个query的分词后的token词条
       for i, v2 := range res { // 遍历每一个结果
          score := calculateTFIDF(v, v2, res) 
          // 记录分数构成,计算每个词条对每个文档结构的score
         if _, ok := exist[i]; !ok { 
          // 如果exist中还没存在这个词条,则进行进行初始化
             tmp := &SortRes{
                Docx:  v2,
                Score: score,
                 Id:    i,
              }
             exist[i] = tmp
          } else {
           // 如果已经存在了,则进行分数的相加
          // 意思就是每个res中的doc对于每个token的权重之和的结果。权重的对象始终都是res中doc
              exist[i].Score += score
           }
        }
     }
     resList := make([]*SortRes, 0)
     for _, v := range exist { // 构建结构体
       resList = append(resList, &SortRes{
          Docx:  v.Docx,
           Score: v.Score,
           Id:    v.Id,
       })
     }
    sort.Slice(resList, func(i, j int) bool { // 按照score进行排序
        return resList[i].Score > resList[j].Score
     })
     return resList
}
  • 演示
func TestSe(t *testing.T) {
    query := "王小波,徐克"
   InitConfig() // 初始化配置
   docx := fileOpen()
    index := BuildIndex(docx) // 创建index
     res, qy := search(index, query, docx)
     fmt.Printf("一共%d记录,query分词结果%v\n", len(res), qy)
     resList := sortRes(qy, res)
    for i := range resList {
       fmt.Println(resList[i].Score, resList[i].Docx)
     }
}

结果:

第一行输出一共多少条搜索记录,然后是输入的query的分词结果
接着输出每一个搜索结果的score,以及对应的docx文本。

一共6记录,query分词结果[小波 王小波 徐克]
39.99999999999999 "王小波的作品《红拂夜奔》将被改编为电影,徐克执导"
20.689655172413794 "王小波经典中篇小说《绿毛水怪》将改编电影。《绿毛水怪》是王小波早期手稿作品,以天马行空的想象,极具魔幻色彩的情感脉络,独树一帜的批评、反讽,受到广大书迷的喜爱。王小波曾创作电影剧本《东宫西宫》,此后尚未有作品改编成电影。据悉,李银河将担任《绿毛水怪》电影版的文学顾问。"
8 "博纳公布新片计划 徐克将开拍《智取威虎山3D》"
3.0769230769230766 "徐克将拍摄电影版《神雕侠侣》三部曲,施南生监制。这是徐克自执导《东方不败风云再起》后,24年来再次拍摄金庸武侠作品,杨过和龙女的故事将登大银幕。自1983年香港邵氏出品制作《杨过与小龙女》电影版后,这部作品34年来都再未出现在大银幕上。"
2.222222222222222 "《抓猴》是一 部徐克导演的现代题材的3D惊悚片,剧情悬疑诡异。影片的主要故事在三个女主演身上展开,在窥视、背叛、阴谋、死亡的惊险不断中,导向一个让人意想不到的结局"
1.4906832298136643 "在去年北影节“跨界与融合—中国电影投融资高峰论坛”上,博纳副总裁丁一岚透露,徐克计划拍《智取威虎山》前传。丁一岚谈到“投资瞄准度”话题时表示博纳是一个“传统的电影公司”,“传统的电影公司会去养一个市场,不会一本万利,我们人人都期待有爆款,可爆款是建立在一将功成万骨枯的基础上,我也不指望所有的项目里面一定有爆款,所以只能按照基础的商业规则去运作每一个项目。接着我们可能启动《智取威虎山》前传,可能还是徐克来导,因为这是用一种新的方式,开发一些被大家忽略的地方。"

当然这个是一个很粗糙的demo,还有很多东西没有,比如如何merge多个倒排索引,如何存储倒排索引,分词如何更好一点,计算排名的权重如何选择和优化等等…

参考

  • https://www.jianshu.com/p/1fa4b0d9a211
  • https://www.syrr.cn/news/22044.html
  • https://chatgpt.shizhuang-inc.com

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Wsl 错误 0x80004002 解决

wsl2安装教程:https://www.jianshu.com/p/6e7488440db2 我安装的过程中出现了如下错误: 解决办法: 已管理员身份运行Powershell运行以下命令以获取包的全名 Get-AppxPackage |? { $_.Name -like "*WindowsSubsystemforLinux*"…

(33)(33.3) 连接实例

文章目录 前言 33.3.1 嵌入在集体PPM/总信号通道中的RSSI 33.3.2 模拟电压型RSSI被输送到一个专用引脚 33.3.3 PWM类型的RSSI输送到一个专用引脚 前言 以下是典型的 RC 接收机 RSSI 连接方案示例: 33.3.1 嵌入在集体PPM/总信号通道中的RSSI 通常的做法是在一根…

NLog写日志到数据库

需求&#xff1a;NLog写日志到数据库 一、必须要安装&#xff1a; System.Data.SqlClient 二、 NLog配置&#xff1a; <?xml version"1.0" encoding"utf-8" ?> <nlog xmlns"http://www.nlog-project.org/schemas/NLog.xsd" …

存css实现动态时钟背景

代码实现 <!DOCTYPE html> <html lang"en"> <head><meta http-equiv"Content-Type" content"text/html; charsetUTF-8"><title>Title</title><meta name"referrer" content"no-referrer…

Spring的控制翻转(IOC)与依赖注入(DI)

SpringIOC 即 Inversion of Control&#xff0c;缩写为 IOC&#xff0c;就是由 Spring IoC 容器管理对象&#xff0c;而非传统实现中由程序代码直接操控. 使用IOC容器管理bean&#xff08;IOC&#xff09; 在IOC容器中将有依赖关系的bean进行关系绑定 最终达到的目的&#…

【C】回调函数和qsort详解

回调函数概念 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针&#xff08;地址&#xff09;作为参数传递给另一 个函数&#xff0c;当这个指针被用来调用其所指向的函数时&#xff0c;我们就说这是回调函数。回调函数不是由该 函数的实现方直接调用&#xff0c;…

七大排序算法——快速排序,通俗易懂的思路讲解与图解(完整Java代码)

文章目录 一、排序的概念排序的概念排序的稳定性七大排序算法 二、快速排序核心思想Hoare法挖坑法前后指针法(选学) 三、性能分析四、算法优化优化基准的选取优化少量数据时的排序方案优化后的完整代码 五、七大排序算法 一、排序的概念 排序的概念 排序&#xff1a;所谓排序…

基于ChatGPT和私有知识库搭建Quivr项目

准备工作 安装docker和docker-compose申请supabase账号 拉取Quivr代码 git clone https://github.com/StanGirard/Quivr.git 复制.XXXXX_env文件 cp .backend_env.example backend/.env cp .frontend_env.example frontend/.env 更新backend/.env和frontend/.env文件 ba…

靶场的安装

sqli-lab 1.将安装包解压放到WWW目录下 2.修改 db-creds.inc文件里面的数据库的用户名密码为自己的用户名密码 路径&#xff1a;D:\phpStudy_64\phpstudy_pro\WWW\sqli-labs-master\sql-connections\db-creds.inc 3. 更改php版本位5.9版本&#xff0c;不然会报错 4.安装数…

【采用有限元法技术计算固有频率和欧拉屈曲荷载】使用有限元法的柱子的固有频率和屈曲荷载(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Android JNI 异常处理 (十一)

🔥 Android Studio 版本 🔥 🔥 创建包含JNI的类 JNIException.java 🔥 package com.cmake.ndk1.jni;public class JNIException {static {System.loadLibrary("exception-lib");}public native void nativeInvokeJavaException();public native void nativ…

B站这些“搬运工”还能有这么高的流量吗?

飞瓜数据&#xff08;B站版&#xff09;观察发现&#xff0c;B站经常有一些搬运视频能够获得超高流量。 比如拉取近15天的B站热门视频&#xff0c;位列前排的就有两个是搬运二创视频&#xff0c;播放量高达900万上下&#xff0c;可以说是爆款视频了。 这些视频有一个相同的点就…

Qt Https通信: TLS initialization failed 解决方法

Qt Https通信&#xff1a; TLS initialization failed 解决方法&#xff0c;Window端使用Qt 做开发请求Https资源时&#xff0c;会经常遇到 TLS initialization failed。 原因分析&#xff1a; 在Qt中并未包含 SSL所包含的库&#xff0c;因此需要开发者&#xff0c;自己将库拷贝…

最新华为鸿蒙4.0安装谷歌服务框架,安装Play商店,谷歌Google,GMS

最近华为推出了最新鸿蒙4.0开发者Beta版本&#xff0c;让用户测试体验。那么测试体验的机器主要是最近发布的几款机器为P60,P60 Pro, mate50,mate50 pro等几款产品可以先期进行体验测试鸿蒙4.0&#xff0c;那么很多的用户在疑问我升级到鸿蒙4.0。是不是还是可以使用Google谷歌服…

LINUX环境小实验

实验报告 实验名称 小环境搭设 实验目的 1.搭建DHCP服务器&#xff08;IP&#xff1a;192.168.100.253静态IP网卡vmnet1&#xff09; 2.搭建DNS&#xff08;通过DHCP服务器分到指定的IP&#xff1a;192.168.100.252&#xff09; 3.搭建网站服务&#xff08;通过DHCP服务器分…

波分复用(WDM)基本原理

文章目录 波分复用WDMDWDM解决问题&#xff0c;特点&#xff0d;超长距离无电中继传输&#xff0c;降低成本 波分系统的基本组成DWDM网元基本类型波分常见站点类型OM/OD技术&#xff0d;波分复用器主要参数 DWDM系统关键技术光转发技术 OM/OD技术&#xff0d;波分复用器件 波分…

Bun 0.6.14发布,1.0版预计发布于9月7日

Bun 是一个 JavaScript 运行时。 Bun 是一个从头开始构建的新 JavaScript 运行时&#xff0c;旨在服务现代 JavaScript 生态系统。它有三个主要设计目标&#xff1a; 速度。包子启动快&#xff0c;运行也快。它扩展了 JavaScriptCore&#xff0c;即为 Safari 构建的注重性能的 …

Office如何通过VSTO进行PPT插件开发?

文章目录 0.引言1.工具准备2.PPT外接程序创建和生成3.外接程序生成并使用 0.引言 VSTO&#xff08;Visual Studio Tools for Office &#xff09;是VBA的替代&#xff0c;是一套用于创建自定义Office应用程序的Visual Studio工具包。VSTO可以用Visual Basic 或者Visual C#扩展O…

EIK+Filebeat+Kafka

目录 Kafka 概述 为什么需要消息队列&#xff08;MQ&#xff09; 使用消息队列的好处 消息队列的两种模式 Kafka 定义 Kafka 简介 Kafka 的特性 Kafka 系统架构 Partation 数据路由规则&#xff1a; 分区的原因 部署 kafka 集群 1.下载安装包 2.安装 Kafka 修改配…

【979. 在二叉树中分配硬币】

来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 给定一个有 N 个结点的二叉树的根结点 root&#xff0c;树中的每个结点上都对应有 node.val 枚硬币&#xff0c;并且总共有 N 枚硬币。 在一次移动中&#xff0c;我们可以选择两个相邻的结点&#x…