Go/Golang 是我最喜欢的语言之一;我喜欢极简主义和它的干净,它的语法非常紧凑,并且努力让事情变得简单(我是 KISS 原则的忠实粉丝)。
我最近面临的主要挑战之一是构建一个快速的搜索引擎。当然还有 SOLR 和 ElasticSearch 等选项;两者都工作得很好并且具有高度可扩展性,但是,我需要简化搜索,使其更快、更容易部署,几乎没有依赖项。
我需要进行足够的优化才能快速返回结果,以便可以对它们进行重新排名。虽然 C/Rust 可能很适合这个,但我更看重开发速度和生产力。我认为 Golang 是两全其美的。
在本文中,我将通过一个简单的示例来介绍如何使用 Go 构建自己的搜索引擎,你会惊讶地发现:它并没有您想象的那么复杂。
我不知道为什么,但 Golang 在某种程度上感觉像 Python。语法很容易掌握,也许是因为到处都没有分号和括号,或者是没有丑陋的 try-catch 语句。也许这是很棒的 Go 格式化程序,我不知道。
无论如何,由于 Golang 生成一个独立的二进制文件,因此部署到任何生产服务器都非常容易。您只需“构建”并交换可执行文件即可。
这正是我所需要的。
不,这不是打字错误吗? Bleve 是一个功能强大、易于使用且非常灵活的 Golang 搜索库。
作为 Go 开发人员,您通常会像躲避瘟疫一样避免使用 3rd 方包;有时使用第三方软件包是有意义的。 Bleve 速度快、设计精良,并提供足够的价值来证明使用它的合理性。
此外,这就是我“Bleve”的原因:
独立,Golang 的一大优点是单一二进制文件,所以我想保持这种感觉,不需要外部数据库或服务来存储和查询文档。 Bleve 与 Sqlite 类似,在内存中运行并写入磁盘。
易于扩展。由于它只是 Go 代码,因此我可以根据需要轻松调整库或在我的代码库中扩展它。
快速:1000 万个文档的搜索结果只需 50-100 毫秒,其中包括过滤。
分面:如果没有一定程度的分面支持,您就无法构建现代搜索引擎。 Bleve 完全支持常见的构面类型:例如范围或简单类别计数。
快速索引:Bleve 比 SOLR 稍慢。 SOLR 可以在 30 分钟内索引 1000 万个文档,而 Bleve 需要一个多小时,但是一个小时左右仍然相当不错,速度足以满足我的需求。
良好的质量结果。 Bleve 在关键字结果方面表现出色,而且一些语义类型搜索在 Bleve 中也非常有效。
快速启动:如果您需要重新启动或部署更新,只需几毫秒即可重新启动 Bleve。在内存中重建索引时不会阻塞读取,因此重新启动后几毫秒内即可搜索索引,不会出现中断。
在 Bleve 中,“索引”可以被视为数据库表或集合(NoSQL)。与常规 SQL 表不同,您不需要指定每一列,基本上可以在大多数用例中使用默认架构。
要初始化 Bleve 索引,您可以执行以下操作:
mappings := bleve.NewIndexMapping() index, err = bleve.NewUsing("/some/path/index.bleve", mappings, "scorch", "scorch", nil) if err != nil { log.Fatal(err) }
Bleve 支持几种不同的索引类型,但经过多次摆弄后我发现“scorch”索引类型可以为您提供最佳性能。如果您不传入最后 3 个参数,Bleve 将默认为 BoltDB。
向 Bleve 添加文档非常简单。您基本上可以在索引中存储任何类型的结构:
type Book struct { ID int `json:"id"` Name string `json:"name"` Genre string `json:"genre"` } b := Book{ ID: 1234, Name: "Some creative title", Genre: "Young Adult", } idStr := fmt.Sprintf("%d", b.ID) // index(string, interface{}) index.index(idStr, b)
如果您要索引大量文档,最好使用批处理:
// You would also want to check if the batch exists already // - so that you don't recreate it. batch := index.NewBatch() if batch.Size() >= 1000 { err := index.Batch(batch) if err != nil { // failed, try again or log etc... } batch = index.NewBatch() } else { batch.index(idStr, b) }
正如您所注意到的,使用“index.NewBatch”可以简化诸如批处理记录并将其写入索引之类的复杂任务,该任务创建一个容器来临时索引文档。
此后,您只需在循环时检查大小,并在达到批量大小限制后刷新索引。
Bleve 公开了多个不同的搜索查询解析器,您可以根据您的搜索需求进行选择。为了使本文简短而有趣,我将使用标准查询字符串解析器。
searchParser := bleve.NewQueryStringQuery("chicken reciepe books") maxPerPage := 50 ofsset := 0 searchRequest := bleve.NewSearchRequestOptions(searchParser, maxPerPage, offset, false) // By default bleve returns just the ID, here we specify // - all the other fields we would like to return. searchRequest.Fields = []string{"id", "name", "genre"} searchResults, err := index.Search(searchResult)
只需这几行,您现在就拥有了一个强大的搜索引擎,可以以较低的内存和资源占用提供良好的结果。
这是搜索结果的 JSON 表示,“hits”将包含匹配的文档:
{ "status": { "total": 5, "failed": 0, "successful": 5 }, "request": {}, "hits": [], "total_hits": 19749, "max_score": 2.221337297308545, "took": 99039137, "facets": null }
如前所述,Bleve 提供开箱即用的全面分面支持,而无需在您的架构中进行设置。以《流派》一书为例,您可以执行以下操作:
//... build searchRequest -- see previous section. // Add facets genreFacet := bleve.NewFacetRequest("genre", 50) searchRequest.AddFacet("genre", genreFacet) searchResults, err := index.Search(searchResult)
我们仅用 2 行代码扩展了之前的 searchRequest。 “NewFacetRequest”接受 2 个参数:
字段:索引中要分面的字段(字符串)。
大小:要计数的条目数(整数)。因此,在我们的示例中,它只会计算前 50 个流派。
以上内容现在将填充我们搜索结果中的“方面”。
接下来,我们只需将我们的方面添加到搜索请求中即可。它接受“方面名称”和实际方面。 “Facet name”是您将在我们的搜索结果中找到此结果集的“键”。
虽然“QueryStringQuery”解析器可以为您带来相当多的帮助;有时您需要更复杂的查询,例如“一个必须匹配”,您希望将搜索词与多个字段进行匹配,并返回结果,只要至少有一个字段匹配即可。
您可以使用“Disjunction”和“Conjunction”查询类型来完成此操作。
联合查询:基本上,它允许您将多个查询链接在一起形成一个巨大的查询。所有子查询必须至少匹配一个文档。
析取查询:这将允许您执行上面提到的“一个必须匹配”查询。您可以传入 x 数量的查询,并设置必须匹配至少一个文档的子查询数量。
析取查询示例:
mappings := bleve.NewIndexMapping() index, err = bleve.NewUsing("/some/path/index.bleve", mappings, "scorch", "scorch", nil) if err != nil { log.Fatal(err) }
与我们之前使用“searchParser”的方式类似,我们现在可以将“析取查询”传递到“searchRequest”的构造函数中。
虽然不完全相同,但类似于以下 SQL:
type Book struct { ID int `json:"id"` Name string `json:"name"` Genre string `json:"genre"` } b := Book{ ID: 1234, Name: "Some creative title", Genre: "Young Adult", } idStr := fmt.Sprintf("%d", b.ID) // index(string, interface{}) index.index(idStr, b)
您还可以通过设置“query.Fuzziness=[0 or 1 or 2]”来调整搜索的模糊程度
连接查询示例:
// You would also want to check if the batch exists already // - so that you don't recreate it. batch := index.NewBatch() if batch.Size() >= 1000 { err := index.Batch(batch) if err != nil { // failed, try again or log etc... } batch = index.NewBatch() } else { batch.index(idStr, b) }
您会注意到语法非常相似,您基本上可以互换使用“Conjunction”和“Disjunction”查询。
这将类似于 SQL 中的以下内容:
searchParser := bleve.NewQueryStringQuery("chicken reciepe books") maxPerPage := 50 ofsset := 0 searchRequest := bleve.NewSearchRequestOptions(searchParser, maxPerPage, offset, false) // By default bleve returns just the ID, here we specify // - all the other fields we would like to return. searchRequest.Fields = []string{"id", "name", "genre"} searchResults, err := index.Search(searchResult)
总结一下;当您希望所有子查询匹配至少一个文档时,请使用“联合查询”;当您希望匹配至少一个子查询但不一定匹配所有子查询时,请使用“析取查询”。
如果您遇到速度问题,Bleve 还可以将数据分布在多个索引分片上,然后在一个请求中查询这些分片,例如:
{ "status": { "total": 5, "failed": 0, "successful": 5 }, "request": {}, "hits": [], "total_hits": 19749, "max_score": 2.221337297308545, "took": 99039137, "facets": null }
分片可能会变得相当复杂,但正如您在上面看到的,Bleve 消除了很多痛苦,因为它会自动“合并”所有索引并在它们之间进行搜索,然后在一个结果集中返回结果,就像您搜索一样单个索引。
我一直在使用分片来搜索 100 个分片。整个搜索过程平均只需 100-200 毫秒即可完成。
您可以按如下方式创建分片:
//... build searchRequest -- see previous section. // Add facets genreFacet := bleve.NewFacetRequest("genre", 50) searchRequest.AddFacet("genre", genreFacet) searchResults, err := index.Search(searchResult)
只要确保为每个文档创建唯一的 ID,或者采用某种可预测的方式添加和更新文档,而不会弄乱索引。
执行此操作的一个简单方法是将包含分片名称的前缀存储在源数据库中或从您获取文档的任何位置。这样,每次您尝试插入或更新时,您都会查找“前缀”,它会告诉您在哪个分片上调用“.index”。
说到更新,只需调用“index.index(idstr, struct)”即可更新现有文档。
仅使用上面的这种基本搜索技术并将其置于 GIN 或标准 Go HTTP 服务器后面,您就可以构建非常强大的搜索 API 并服务数百万个请求,而无需推出复杂的基础设施。
但有一点需要注意;但是,Bleve 不支持复制,因为您可以将其包装在 API 中。只需有一个 cron 作业,从您的源中读取数据,然后使用 goroutine 将更新“爆炸”到您的所有 Bleve 服务器。
或者,您可以将写入磁盘锁定几秒钟,然后将数据“rsync”到从属索引,尽管我不建议这样做,因为您可能还需要每次重新启动 go 二进制文件.
以上是Bleve:如何构建一个快速的搜索引擎?的详细内容。更多信息请关注PHP中文网其他相关文章!