news 2026/8/19 4:11:51

Go搜索引擎:Elasticsearch与Meilisearch

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Go搜索引擎:Elasticsearch与Meilisearch

Go搜索引擎:Elasticsearch与Meilisearch

摘要: 本篇讲解Go语言集成Elasticsearch和Meilisearch两个搜索引擎,用olivere/elastic实现索引CRUD和全文搜索加聚合分析,用Meilisearch的轻量HTTP API实现即时搜索,分享ES mapping字段类型设置错误导致查询无结果的踩坑经验,对比Elasticsearch、Meilisearch和数据库LIKE三种搜索方案。

开篇故事

我们的商品搜索原先是MySQL的LIKE查询,商品表30万条数据。用户搜"无线耳机",LIKE '%无线%'扫全表,平均耗时1.2秒,高峰期2秒以上。产品要求改成毫秒级返回,还要支持按价格区间过滤、按销量排序、按品牌聚合。

先上了Elasticsearch,把30万商品数据同步过去。全文搜索+过滤+排序组合查询,平均返回时间降到30毫秒。但ES的mapping配置有坑,踩过一次查询死活不出结果,排查半天发现是字段类型设错了。

后来有个内部小项目只需要简单全文搜索,数据量不到5万条,上ES太重了。发现Meilisearch,单二进制部署,开箱即用,索引5万条数据搜索延迟在10毫秒以内。这篇把两个搜索引擎的Go集成写清楚。

一、Elasticsearch索引与搜索

olivere/elastic是Go操作ES最成熟的客户端库。核心概念是索引(类似数据库表)、文档(类似行记录)、mapping(类似表结构)。先建索引设mapping,再写文档,最后搜索。

先看索引创建和文档写入。mapping定义字段类型,text类型会分词建立倒排索引,keyword类型不分词用于精确匹配和聚合。

packagesearchimport("context""encoding/json""errors""log""strconv""time""github.com/olivere/elastic/v7")// Product 商品结构体typeProductstruct{IDint64`json:"id"`// 商品IDNamestring`json:"name"`// 商品名称,全文搜索Brandstring`json:"brand"`// 品牌,精确匹配Categorystring`json:"category"`// 分类,聚合Pricefloat64`json:"price"`// 价格,范围过滤Salesint`json:"sales"`// 销量,排序CreatedAtstring`json:"created_at"`// 创建时间}// ESClient Elasticsearch客户端封装typeESClientstruct{client*elastic.Client// olivere/elastic客户端}// NewESClient 创建ES客户端// url: ES地址,如"http://127.0.0.1:9200"funcNewESClient(urlstring)(*ESClient,error){// 创建客户端,设置超时和重试client,err:=elastic.NewClient(elastic.SetURL(url),// 请求超时10秒elastic.SetTimeout(10*time.Second),// 健康检查elastic.SetSniff(false),// 错误日志elastic.SetErrorLog(log.Default()),)iferr!=nil{returnnil,err}return&ESClient{client:client},nil}// CreateIndex 创建商品索引// indexName: 索引名,如"products"func(c*ESClient)CreateIndex(ctx context.Context,indexNamestring)error{// 先检查索引是否已存在exists,err:=c.client.IndexExists(indexName).Do(ctx)iferr!=nil{returnerr}ifexists{returnerrors.New("索引已存在: "+indexName)}// 定义mapping,指定每个字段的类型mapping:=`{ "mappings": { "properties": { "id": { "type": "long" }, "name": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "brand": { "type": "keyword" }, "category": { "type": "keyword" }, "price": { "type": "double" }, "sales": { "type": "integer" }, "created_at":{ "type": "date" } } } }`// 创建索引并应用mapping_,err=c.client.CreateIndex(indexName).Body(mapping).Do(ctx)returnerr}// IndexDoc 写入单个文档// indexName: 索引名// doc: 商品文档func(c*ESClient)IndexDoc(ctx context.Context,indexNamestring,doc*Product)error{// 用商品ID作为文档ID,保证幂等_,err:=c.client.Index().Index(indexName).Id(strconv.FormatInt(doc.ID,10)).BodyJson(doc).Do(ctx)returnerr}// BulkIndex 批量写入文档// 批量写入比逐条写入快很多,减少网络往返func(c*ESClient)BulkIndex(ctx context.Context,indexNamestring,docs[]*Product)error{// 创建批量处理器bulk:=c.client.Bulk()for_,doc:=rangedocs{// 每个文档一个Index请求req:=elastic.NewBulkIndexRequest().Index(indexName).Doc(doc)bulk=bulk.Add(req)}// 一次性提交_,err:=bulk.Do(ctx)returnerr}

搜索是ES的核心能力。bool查询组合must(必须匹配)、filter(过滤不评分)、should(可选匹配)。aggregations做聚合统计。

// SearchResult 搜索结果typeSearchResultstruct{Totalint64`json:"total"`// 总匹配数Items[]*Product`json:"items"`// 商品列表Brandsmap[string]int64`json:"brands"`// 品牌聚合}// SearchProducts 搜索商品// keyword: 搜索关键词// minPrice, maxPrice: 价格区间// brand: 品牌过滤,空则不过滤// from, size: 分页func(c*ESClient)SearchProducts(ctx context.Context,indexName,keywordstring,minPrice,maxPricefloat64,brandstring,from,sizeint)(*SearchResult,error){// 构造bool查询boolQuery:=elastic.NewBoolQuery()// 关键词全文搜索,放在must里参与评分ifkeyword!=""{matchQuery:=elastic.NewMatchQuery("name",keyword)boolQuery=boolQuery.Must(matchQuery)}// 价格区间过滤,放在filter里不参与评分priceRange:=elastic.NewRangeQuery("price").Gte(minPrice).Lte(maxPrice)boolQuery=boolQuery.Filter(priceRange)// 品牌精确匹配ifbrand!=""{brandTerm:=elastic.NewTermQuery("brand",brand)boolQuery=boolQuery.Filter(brandTerm)}// 构造搜索请求search:=c.client.Search().Index(indexName).Query(boolQuery).From(from).// 分页起始位置Size(size).// 每页数量// 按销量降序,销量相同按价格升序Sort("sales",false).Sort("price",true)// 添加品牌聚合,统计每个品牌的商品数量brandAgg:=elastic.NewTermsAggregation().Field("brand")search=search.Aggregation("brands",brandAgg)// 执行搜索res,err:=search.Do(ctx)iferr!=nil{returnnil,err}// 解析结果result:=&SearchResult{Total:res.TotalHits(),Items:make([]*Product,0),Brands:make(map[string]int64),}// 遍历命中文档for_,hit:=rangeres.Hits.Hits{varp Productiferr:=json.Unmarshal(hit.Source,&p);err!=nil{continue}result.Items=append(result.Items,&p)}// 解析聚合结果ifbrands,found:=res.Aggregations.Terms("brands");found{for_,bucket:=rangebrands.Buckets{result.Brands[bucket.Key.(string)]=bucket.DocCount}}returnresult,nil}

全文搜索放must里参与相关度评分,过滤条件放filter里不评分只筛选。filter不评分性能更好,ES会对filter结果做缓存。聚合放顶层aggregations里,搜索同时返回聚合统计。

二、Meilisearch轻量替代

Meilisearch比ES轻很多,单个二进制文件部署,没有复杂的集群配置。适合数据量中小、搜索场景简单的项目。Go通过HTTP API集成,也可以用官方Go SDK。

packagesearchimport("bytes""context""encoding/json""fmt""net/http""time")// MeiliClient Meilisearch客户端typeMeiliClientstruct{urlstring// Meilisearch地址keystring// API密钥http*http.Client// HTTP客户端}// NewMeiliClient 创建Meilisearch客户端// url: 地址,如"http://127.0.0.1:7700"// key: API密钥,无鉴权可传空funcNewMeiliClient(url,keystring)*MeiliClient{return&MeiliClient{url:url,key:key,http:&http.Client{Timeout:10*time.Second},}}// AddDocuments 批量添加文档// index: 索引名// docs: 文档列表,自动序列化JSONfunc(c*MeiliClient)AddDocuments(ctx context.Context,indexstring,docsinterface{})error{// 序列化文档列表body,err:=json.Marshal(docs)iferr!=nil{returnerr}// 构造请求url:=fmt.Sprintf("%s/indexes/%s/documents",c.url,index)req,err:=http.NewRequestWithContext(ctx,"POST",url,bytes.NewReader(body))iferr!=nil{returnerr}req.Header.Set("Content-Type","application/json")ifc.key!=""{req.Header.Set("Authorization","Bearer "+c.key)}// 发送请求resp,err:=c.http.Do(req)iferr!=nil{returnerr}deferresp.Body.Close()returnnil}// Search 执行搜索// query: 搜索关键词// filter: 过滤表达式,如"price >= 50 AND price <= 200"// limit: 返回数量func(c*MeiliClient)Search(ctx context.Context,index,query,filterstring,limitint)(map[string]interface{},error){// 构造搜索请求体searchReq:=map[string]interface{}{"q":query,"limit":limit,}iffilter!=""{searchReq["filter"]=filter}body,_:=json.Marshal(searchReq)url:=fmt.Sprintf("%s/indexes/%s/search",c.url,index)req,err:=http.NewRequestWithContext(ctx,"POST",url,bytes.NewReader(body))iferr!=nil{returnnil,err}req.Header.Set("Content-Type","application/json")ifc.key!=""{req.Header.Set("Authorization","Bearer "+c.key)}resp,err:=c.http.Do(req)iferr!=nil{returnnil,err}deferresp.Body.Close()// 解析响应varresultmap[string]interface{}json.NewDecoder(resp.Body).Decode(&result)returnresult,nil}// UpdateSearchableAttributes 设置可搜索字段// 默认所有字段都可搜索,指定后只搜索指定字段func(c*MeiliClient)UpdateSearchableAttributes(ctx context.Context,indexstring,fields[]string)error{body,_:=json.Marshal(map[string]interface{}{"searchableAttributes":fields})url:=fmt.Sprintf("%s/indexes/%s/settings",c.url,index)req,_:=http.NewRequestWithContext(ctx,"PATCH",url,bytes.NewReader(body))req.Header.Set("Content-Type","application/json")ifc.key!=""{req.Header.Set("Authorization","Bearer "+c.key)}resp,err:=c.http.Do(req)iferr!=nil{returnerr}resp.Body.Close()returnnil}

Meilisearch开箱支持拼写纠错和前缀匹配,搜索体验比ES默认配置好。filter语法简单,类似SQL表达式。适合不需要复杂聚合的场景。

三、踩坑经验:ES mapping类型错误导致查询无结果

这个坑排查了半天。商品索引的price字段,第一版mapping里设成了text类型。写入数据正常,文档能查到。但价格区间过滤怎么都查不出结果,price范围查询返回0条命中。

// 错误的mapping,price设成了textbadMapping:=`{ "mappings": { "properties": { "name": { "type": "text" }, "price": { "type": "text" } } } }`

text类型会被分词器处理。写入价格99.9,分词后变成"99"和"9"两个token。range查询要在数字类型上操作,text类型没有数值范围,自然查不出结果。

ES的字段类型一旦索引了文档就不能改。要修mapping必须删索引重建。

// 修复步骤: 删除旧索引,用正确mapping重建func(c*ESClient)FixMapping(ctx context.Context,indexNamestring)error{// 删除旧索引_,err:=c.client.DeleteIndex(indexName).Do(ctx)iferr!=nil{// 索引不存在不算错误log.Printf("删除索引: %v",err)}// 用正确的mapping重建// price必须是double或scaled_float类型mapping:=`{ "mappings": { "properties": { "name": { "type": "text", "analyzer": "ik_max_word" }, "price": { "type": "double" }, "brand": { "type": "keyword" } } } }`_,err=c.client.CreateIndex(indexName).Body(mapping).Do(ctx)returnerr}

踩过这个坑后总结了几条mapping设置原则。数值字段用integer或double,需要范围过滤绝不能用text。品牌、分类等需要精确匹配和聚合的字段用keyword。只有需要全文搜索的字段用text。text和keyword的区别是text会分词,keyword不分词整体存储。一个字段既要全文搜索又要精确匹配,可以用multi-field,主字段text加一个子字段keyword。

// multi-field: name既能全文搜索又能精确匹配multiMapping:=`{ "mappings": { "properties": { "name": { "type": "text", "analyzer": "ik_max_word", "fields": { "keyword": { "type": "keyword" } } } } } }`

上线前先用少量数据验证mapping,确认所有查询都能正常返回结果,再全量同步。避免数据同步完了才发现mapping有问题,重建索引费时费力。

四、对比分析

特性ElasticsearchMeilisearchMySQL LIKE
搜索性能高(毫秒级)高(毫秒级)低(秒级)
部署复杂度高(集群+JVM)低(单二进制)无(随数据库)
全文搜索强(分词+评分)强(开箱即用)弱(只有LIKE)
聚合分析支持不支持GROUP BY
资源占用高(1G起步)低(百M级)
数据量上限亿级千万级百万级

数据量百万以内且只有简单搜索需求,数据库LIKE勉强能用但性能差。数据量千万以内搜索需求简单,Meilisearch部署简单搜索体验好。数据量千万以上需要复杂聚合和评分,Elasticsearch是标准选择。三者的资源占用差距很大,ES动辄几个G内存,Meilisearch几百M,选型要考虑运维成本。

总结

Elasticsearch用olivere/elastic集成,mapping里数值字段用数值类型,文本搜索用text,精确匹配用keyword,复杂查询用bool组合must和filter。Meilisearch单二进制部署,HTTP API集成简单,适合中小数据量的即时搜索。mapping类型设错会导致查询无结果,上线前用少量数据验证。选型看数据量和搜索复杂度,别对小数据量上ES,也别对大数据量指望LIKE。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/19 4:11:31

Mac本地部署多Grok智能体:会话隔离与批量自动化实践

这次我们来看一个名为“Lots of Agents”的项目&#xff0c;它解决了一个非常具体且有趣的问题&#xff1a;如何在一台Mac电脑上&#xff0c;运行无限多个已登录状态的Grok机器人&#xff08;Bots&#xff09;。对于需要批量操作、自动化测试或数据采集的开发者来说&#xff0c…

作者头像 李华
网站建设 2026/8/19 4:10:21

基于ESP32-S3与LVGL打造本地化智能家居控制中心实战

1. 项目缘起&#xff1a;为什么需要一个“物理化”的智能家居控制中心&#xff1f;作为一名智能家居的深度折腾用户&#xff0c;我几乎把所有能接入的设备都接入了Home Assistant。手机App、网页端、平板挂墙&#xff0c;这些方案我都试过&#xff0c;但总觉得差点意思。手机Ap…

作者头像 李华
网站建设 2026/8/19 4:10:05

奥迪Q2L成都车展预售策略解析:产品定位、市场博弈与消费者决策指南

1. 从“亮相”到“预售”&#xff1a;一次车展背后的产品逻辑与市场博弈 成都车展&#xff0c;对于汽车行业而言&#xff0c;从来不只是简单的“新车亮相”。它更像一个精心设计的舞台&#xff0c;一次面向核心市场与潜在用户的集中火力展示。当“奥迪Q2L将于成都车展亮相并开启…

作者头像 李华
网站建设 2026/8/19 4:08:55

构建堆栈监控器:从原理到实践,实现系统可观测性

1. 项目概述&#xff1a;为什么我们需要一个堆栈监控器&#xff1f;在开发和运维的世界里&#xff0c;系统就像一台精密的仪器&#xff0c;而堆栈&#xff08;Stack&#xff09;则是其核心的“动力总成”。无论是Web应用的后端服务、数据处理管道&#xff0c;还是微服务架构中的…

作者头像 李华
网站建设 2026/8/19 4:08:50

树莓派4B与HQ Camera打造高画质相机工作站:从硬件选型到软件实现

1. 项目概述&#xff1a;当树莓派4B遇上HQ相机与屏幕最近在工作室整理设备&#xff0c;翻出来一块闲置的树莓派4B和官方HQ Camera模块&#xff0c;看着它们&#xff0c;一个念头就冒了出来&#xff1a;为什么不把它们组合起来&#xff0c;再配上一块屏幕&#xff0c;做成一个功…

作者头像 李华
网站建设 2026/8/19 4:04:55

基于Arduino与IoT云构建智能家居:从硬件选型到自动化实战

1. 从零到一&#xff1a;为什么选择Arduino与IoT云构建智能家居&#xff1f;如果你和我一样&#xff0c;对家里的灯光、温湿度、门锁这些设备还停留在手动开关的阶段感到厌倦&#xff0c;但又觉得市面上的成品智能家居套装要么太贵&#xff0c;要么不够“好玩”&#xff0c;那A…

作者头像 李华