一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

Meilisearch:轻量搜索引擎的优雅选择:以及它在 RAG 中的应用

时间:2026-07-07 10:53:01 编辑:袖梨 来源:一聚教程网

为什么需要 Meilisearch

大多数个人项目和小型团队的搜索方案,都经历过这样的阶段:

Meilisearch:轻量搜索引擎的优雅选择,以及它在 RAG 中的应用

SELECT * FROM documents WHERE title LIKE '%关键词%' OR content LIKE '%关键词%';

MySQL LIKE 够用吗?数据量小的时候够用。但它有三个硬伤:

  1. 没有中文分词——搜「机器学习」找不到「机器 学习方法」
  2. 全表扫描——数据量一大就慢
  3. 没有相关性排序——结果顺序全靠运气

选 Elasticsearch?它当然是行业标准,但对个人项目来说太重了:

对比项ElasticsearchMeilisearch
内存占用2GB+~100MB
安装方式JDK + 集群配置单二进制文件
中文分词需装插件(IK)内置支持
API 风格复杂的 JSON DSL简洁的 RESTful
上手成本
适用场景日志分析、大型项目个人/小型项目(<10万文档)

结论:如果你的文档量在 10 万以内,Meilisearch 是更务实的选择。不是 Elasticsearch 不好,是杀鸡用牛刀。

Meilisearch 核心特性

1. 开箱即用的中文分词

不需要安装任何插件,Meilisearch 内置中文分词能力。索引时自动拆词,搜索时自动匹配。

2. Typo Tolerance(容错搜索)

搜「meilisearch」能匹配「meilisearc」「meilserch」,基于 Damerau-Levenshtein 距离算法自动纠错。

3. 前缀搜索

输入「数据」即可匹配「数据库」「数据结构」「数据分析」,实时响应。

4. 过滤与排序

支持字段过滤(如按用户 ID、分类 ID 筛选)和多字段排序,满足业务查询需求。

5. 向量搜索(v1.3+)

这是本文的重点——Meilisearch 不仅做关键词搜索,还支持向量检索和混合搜索,让它在 RAG 场景中也能胜任。

快速上手

安装

# macOSbrew install meilisearch# Linuxcurl -L https://install.meilisearch.com | sh# Windows(scoop)scoop install meilisearch# Dockerdocker run -d -p 7700:7700 -v $(pwd)/meili_data:/meili_data getmeili/meilisearch:v1.3 --master-key=your-master-key

启动

meilisearch --master-key=your-master-key

打开 http://localhost:7700,你会看到一个搜索预览界面——对,它自带 Web UI。

基本操作

# 创建索引 + 添加文档(一步到位)curl -X POST 'http://localhost:7700/indexes/articles/documents' -H 'Authorization: Bearer your-master-key' -H 'Content-Type: application/json' --data '[{"id": 1,"title": "Go 并发编程","content": "goroutine 和 channel 是 Go 并发的核心...","tags": ["go", "并发"]}]'# 搜索curl 'http://localhost:7700/indexes/articles/search?q=并发&filter=user_id=1'

就是这么简单。添加文档的那一刻,索引就已经建好了。

Go 集成实战

在我们的知识库项目中,使用 meilisearch-go SDK 集成 Meilisearch。以下是核心代码。

安装 SDK

go get github.com/meilisearch/meilisearch-go

连接与初始化

package servicesimport ("log""github.com/meilisearch/meilisearch-go")type SearchService struct {client meilisearch.ServiceManagerdocIndex stringchunkIndex string}func GetSearchService() *SearchService {cfg := config.LoadConfig()client := meilisearch.New(cfg.Search.Host,meilisearch.WithAPIKey(cfg.Search.APIKey),)svc := &SearchService{client: client,docIndex: cfg.Search.Index,chunkIndex: cfg.Search.Index + "_chunks",}svc.initDocIndex()svc.initChunkIndex()return svc}

文档索引结构

type DocumentIndex struct {ID uint `json:"id"`UserID uint `json:"user_id"`CategoryID *uint`json:"category_id,omitempty"`Titlestring `json:"title"`Contentstring `json:"content"`Summarystring `json:"summary,omitempty"`Tags []string `json:"tags,omitempty"`CategoryName string `json:"category_name,omitempty"`CreatedAtstring `json:"created_at"`UpdatedAtstring `json:"updated_at"`}

索引配置

func (s *SearchService) initDocIndex() {index := s.client.Index(s.docIndex)// 可过滤字段filterable := &[]interface{}{"user_id", "category_id", "tags"}index.UpdateFilterableAttributes(filterable)// 可排序字段sortable := &[]string{"created_at", "updated_at", "title"}index.UpdateSortableAttributes(sortable)// 搜索字段(权重从高到低:标题 > 内容 > 摘要 > 标签)searchable := &[]string{"title", "content", "summary", "tags"}index.UpdateSearchableAttributes(searchable)// 排序规则ranking := &[]string{"words", "typo", "proximity","attribute", "sort", "updated_at:desc",}index.UpdateRankingRules(ranking)}

搜索

func (s *SearchService) Search(keyword string, userID uint, page, pageSize int) (*DocSearchResponse, error) {index := s.client.Index(s.docIndex)filter := fmt.Sprintf("user_id = %d", userID)result, err := index.Search(keyword, &meilisearch.SearchRequest{Filter: filter,HitsPerPage: pageSize,Page: page,})if err != nil {return nil, err}// 解析结果...return response, nil}

文档同步

文档增删改时自动同步索引:

// 创建文档后searchSvc.IndexDocument(doc)// 更新文档后searchSvc.IndexDocument(doc)// 删除文档后searchSvc.DeleteDocument(docID)

前端智能切换

前端根据是否有搜索关键词,智能选择数据源:

async loadDocuments() {if (this.searchKeyword) {// 有关键词 → Meilisearch 全文搜索(中文分词、相关性排序)const res = await searchDocuments({keyword: this.searchKeyword,page: this.currentPage,categoryID: this.selectedCategoryID,})this.documents = res.listthis.total = res.total} else {// 无关键词 → MySQL 列表(完整浏览)const res = await getDocuments({page: this.currentPage,categoryID: this.selectedCategoryID,})this.documents = res.listthis.total = res.total}}

到这里,全文搜索部分就完成了。但 Meilisearch 的能力不止于此——下面进入正题。

向量搜索:Meilisearch 在 RAG 中的应用

什么是 RAG

RAG(Retrieval-Augmented Generation,检索增强生成)的核心思路:

用户提问 → 检索相关文档 → 将文档作为上下文 → LLM 生成回答

关键步骤是「检索相关文档」。传统方案用关键词匹配,但关键词匹配有局限——用户问「如何优化并发性能」,相关文档可能写的是「goroutine 调度器原理」,关键词完全不同,但语义相关。

向量检索解决的就是这个问题:按语义相似度检索,而非关键词匹配。

向量检索原理

  1. 用 Embedding 模型将文本转为高维向量(如 1024 维)
  2. 相似语义的文本在向量空间中距离更近
  3. 查询时计算向量距离,返回最相似的文档

"并发优化" → [0.12, -0.34, 0.56, ...]─┐ ├─ 余弦相似度高 → 匹配!"goroutine 调度" → [0.11, -0.31, 0.54, ...] ─┘"数据库索引" → [0.87, 0.22, -0.15, ...]── 余弦相似度低 → 不匹配

为什么选择 Meilisearch 做向量检索

之前我们的 RAG 架构:

文档 → 切片 → Embedding → 存 MySQLJSON blob)↓查询时 Go 代码手动算余弦相似度

问题很明显:

  • MySQL 没有 ANN(近似最近邻)索引,全表计算相似度
  • 搜索走 Meilisearch,RAG 走 MySQL,两套索引重复存储
  • 同步逻辑复杂,维护成本高

统一到 Meilisearch 后:

文档 → 切片 → Embedding → 存 Meilisearch(_vectors 字段)↓├─ 关键词搜索:全文检索└─ RAG 检索:向量检索 / 混合检索

一个索引,两种检索方式,架构大幅简化。

userProvided 模式

Meilisearch 支持多种 Embedder 模式:OpenAI、HuggingFace、Ollama 等。但我们选择了 userProvided——手动提供向量。

为什么?因为国内常用通义千问、智谱等 Embedding API,Meilisearch 内置不支持。userProvided 模式让我们自由选择 Embedding 模型,只需把计算好的向量交给 Meilisearch 存储和检索。

代码实现

切片索引结构

// ChunkIndex 文档切片索引结构(用于 RAG 向量检索)type ChunkIndex struct {ID uint`json:"id"`UserID uint`json:"user_id"`DocumentID uint`json:"document_id"`ChunkIndex int `json:"chunk_index"`Contentstring`json:"content"`Vectors*VectorEmbedder `json:"_vectors,omitempty"`// Meilisearch 向量字段}// VectorEmbedder 向量嵌入器结构type VectorEmbedder struct {Manual *ManualVector `json:"manual,omitempty"`}// ManualVector 手动向量(userProvided 模式)type ManualVector struct {Embeddings [][]float32 `json:"embeddings"`Regenerate bool`json:"regenerate"`}

_vectors 字段是 Meilisearch 约定的特殊字段名。manual 是我们配置的 Embedder 名称。数据格式:

{"id": 1,"user_id": 1,"document_id": 42,"chunk_index": 0,"content": "goroutine 和 channel 是 Go 并发的核心...","_vectors": {"manual": {"embeddings": [[0.12, -0.34, 0.56, ...]],"regenerate": false}}}

配置 Embedder

func (s *SearchService) initChunkIndex() {index := s.client.Index(s.chunkIndex)// 配置向量嵌入器embedders := map[string]meilisearch.Embedder{"manual": {Source: meilisearch.UserProvidedEmbedderSource,Dimensions: 1024, // 通义千问 text-embedding-v3 输出维度},}index.UpdateEmbedders(embedders)// 设置可过滤字段filterable := &[]interface{}{"user_id", "document_id"}index.UpdateFilterableAttributes(filterable)}

Dimensions 必须与你的 Embedding 模型输出维度一致。通义千问 text-embedding-v3 输出 1024 维,所以这里设 1024。

索引切片

func (s *SearchService) IndexChunks(documentID, userID uint, chunks []string, embeddings [][]float64) error {var chunkIndices []ChunkIndexfor i, chunk := range chunks {// float64 → float32(Meilisearch 要求 float32)vec := make([]float32, len(embeddings[i]))for j, v := range embeddings[i] {vec[j] = float32(v)}chunkIndices = append(chunkIndices, ChunkIndex{ID: uint(i + 1),UserID: userID,DocumentID: documentID,ChunkIndex: i,Content:chunk,Vectors: &VectorEmbedder{Manual: &ManualVector{Embeddings: [][]float32{vec},Regenerate: false,},},})}// 先清理该文档的旧索引s.DeleteChunksByDocument(documentID)// 批量写入_, err := s.client.Index(s.chunkIndex).AddDocumentsWithContext(context.Background(),&meilisearch.DocumentOptions{PrimaryKey: "id"},chunkIndices,)return err}

向量搜索

func (s *SearchService) VectorSearch(queryVec []float64, userID uint, docIDs []uint, limit int) ([]ChunkSearchResult, error) {// float64 → float32vec := make([]float32, len(queryVec))for i, v := range queryVec {vec[i] = float32(v)}// 构建过滤条件filter := fmt.Sprintf("user_id = %d", userID)result, err := s.client.Index(s.chunkIndex).Search("", &meilisearch.SearchRequest{Vector: vec,Filter: filter,Limit:int64(limit),Hybrid: &meilisearch.SearchRequestHybrid{SemanticRatio: 1.0,// 纯向量搜索Embedder:"manual",},})if err != nil {return nil, err}// 解析结果...return results, nil}

SemanticRatio 控制搜索模式:

  • 1.0:纯向量搜索(语义匹配)
  • 0.0:纯关键词搜索
  • 0.5:混合搜索(各占一半权重)

混合搜索

func (s *SearchService) HybridSearch(queryVec []float64, keyword string, userID uint, limit int, ratio float64) ([]ChunkSearchResult, error) {vec := make([]float32, len(queryVec))for i, v := range queryVec {vec[i] = float32(v)}result, err := s.client.Index(s.chunkIndex).Search(keyword, &meilisearch.SearchRequest{Vector: vec,Filter: fmt.Sprintf("user_id = %d", userID),Limit:int64(limit),Hybrid: &meilisearch.SearchRequestHybrid{SemanticRatio: ratio,// 0.5 = 关键词和向量各半Embedder:"manual",},})if err != nil {return nil, err}// 解析结果...return results, nil}

混合搜索是 Meilisearch 的杀手级特性——一次查询同时考虑关键词匹配和语义相似度,取两者最优结果。

RAG 流程整合

func (s *RAGService) SearchSimilarChunks(query string, userID uint, docIDs []uint, topK int) ([]SearchResult, error) {// 1. 获取查询向量queryVec, err := s.embeddingSvc.GetEmbedding(query)if err != nil {return nil, err}// 2. 优先使用 Meilisearch 向量搜索chunkResults, err := s.searchSvc.VectorSearch(queryVec, userID, docIDs, topK)if err == nil && len(chunkResults) > 0 {return chunkResults, nil}// 3. 降级到 MySQL 余弦相似度log.Printf("Meilisearch 向量搜索失败,降级到 MySQL: %v", err)return s.searchByMySQL(queryVec, userID, docIDs, topK)}

降级策略很重要——Meilisearch 挂了,RAG 还能工作。MySQL 中的 embedding 备份此时派上用场。

完整架构

最终架构非常清晰:

用户请求├─ 关键词搜索 → Meilisearch 全文检索 → 结果└─ AI 对话├─ 用户提问 → Embedding API → 查询向量├─ Meilisearch 向量搜索 → 相似文档切片├─ (降级)MySQL 余弦相似度├─ 拼接 Prompt:问题 + 上下文└─ LLM 生成回答 → 流式输出

一个 Meilisearch 实例,同时服务两种截然不同的搜索需求。

踩坑记录

1. meilisearch-go v0.36 API 不兼容

SDK v0.36 与文档示例有多处不兼容:

问题解决
SearchResult 重复定义hit.DecodeInto() 解码
Total 字段不存在改用 EstimatedTotalHits
UpdateFilterableAttributes*[]interface{}不能传 *[]string
AddDocumentsWithContext 第三参数是 *DocumentOptions&DocumentOptions{PrimaryKey: "id"}
UpdateEmbedders 参数是 map[string]Embedder传值而非指针
Hit 类型是 map[string]interface{}手动映射到目标结构体

2. float64 vs float32

通义千问 Embedding API 返回 float64,Meilisearch SDK 要求 float32。必须手动转换,不能直接传。

3. _vectors 字段格式

_vectors 的格式是 {embedder_name: {embeddings: [[vec]], regenerate: false}},不是简单的向量数组。embeddings 是二维数组——因为一个文档可以有多个向量(虽然我们每个切片只有一个)。

4. Settings 更新是异步的

UpdateFilterableAttributesUpdateEmbedders 等操作是异步的,返回 TaskInfo。如果紧接着搜索,可能配置还没生效。生产环境应该等待 task 完成。

总结

Meilisearch 在个人/小型项目中是 Elasticsearch 的优雅替代品:

  • 轻量:单二进制,~100MB 内存,5 分钟上手
  • 中文友好:内置分词,无需插件
  • API 简洁:RESTful,SDK 支持多语言
  • 向量搜索:userProvided 模式让你自由选择 Embedding 模型
  • 混合搜索:关键词 + 语义一次搞定

在 RAG 场景中,将全文索引和向量索引统一到 Meilisearch,大幅简化了架构——一个搜索服务同时支撑关键词搜索和语义检索,维护成本降低,代码更清晰。

如果你的项目也在 10 万文档以内,试试 Meilisearch,你会发现搜索这件事可以这么简单。

本文基于知识库项目的实际开发经验,项目使用 Go + Gin + Vue 3,Meilisearch 同时服务全文搜索和 RAG 向量检索。

热门栏目