当前位置: 首页
AI教程
Meilisearch轻量搜索引擎的优雅选择及其RAG应用

Meilisearch轻量搜索引擎的优雅选择及其RAG应用

热心网友 时间:2026-07-07
转载

为什么选择 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 自带中文分词能力。索引时自动拆词,搜索时自动匹配,省去了配置 IK 分词器的繁琐步骤。

2. Typo Tolerance(容错搜索)

输入「meilisearch」可匹配「meilisearc」「meilserch」,基于 Damerau-Levenshtein 距离算法自动纠错。用户手滑打错字也能获得理想结果。

3. 前缀搜索

输入「数据」即可匹配「数据库」「数据结构」「数据分析」,并实现实时响应,极大提升用户体验。

4. 过滤与排序

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

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

这是本文的重点——Meilisearch 不仅提供关键词搜索,还支持向量检索和混合搜索,使其在 RAG 场景中同样胜任。

快速上手 Meilisearch

安装

# macOS
brew install meilisearch
# Linux
curl -L https://install.meilisearch.com | sh
# Windows(scoop)
scoop install meilisearch
# Docker
docker 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 services

import (
    "log"
    "github.com/meilisearch/meilisearch-go"
)

type SearchService struct {
    client     meilisearch.ServiceManager
    docIndex   string
    chunkIndex 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"`
    Title        string  `json:"title"`
    Content      string  `json:"content"`
    Summary      string  `json:"summary,omitempty"`
    Tags         []string `json:"tags,omitempty"`
    CategoryName string  `json:"category_name,omitempty"`
    CreatedAt    string  `json:"created_at"`
    UpdatedAt    string  `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.list
        this.total = res.total
    } else {
        // 无关键词 → MySQL 列表(用于完整浏览)
        const res = await getDocuments({
            page:      this.currentPage,
            categoryID:this.selectedCategoryID,
        })
        this.documents = res.list
        this.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 → 存 MySQL(JSON 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"`
    Content     string           `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 []ChunkIndex
    for 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 → float32
    vec := 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 向量检索。

来源:https://juejin.cn/post/7638811024848846883

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
大模型API连续对话交互:上下文持久化与Token节流实践

大模型API连续对话交互:上下文持久化与Token节流实践

一、引言 现在的大模型应用落地上,光靠单次独立问答已经远远不够用了。无论是办公协同的智能体、行业咨询机器人、专属业务问答系统,还是私有化部署的大模型,都得能支撑连续多轮对话、跨会话二次访问、长周期上下文关联问答这些核心能力。 在实际对话中,大家都会碰到一些共性问题:第一轮提问回复正常,第二轮就完全没

时间:2026-07-07 15:44
代驾系统搭建方案:订单调度与司机匹配机制

代驾系统搭建方案:订单调度与司机匹配机制

在城市夜生活日益丰富的当下,代驾早已超越“酒后找人代开”的单一场景,逐步演变为高频、即时、强时效的本地生活服务。无论是商务应酬后的返程,还是临时需要安全送车回家,用户最核心的诉求始终围绕三点:能否快速响应、司机是否靠谱、整个流程是否稳定。对于系统开发者而言,代驾平台搭建的难点恰恰也在于此——它并非简

时间:2026-07-07 15:44
独立开发者上云避坑:阿里云OPC节省两周配置时间

独立开发者上云避坑:阿里云OPC节省两周配置时间

独立项目上云,说起来简单,做起来全是坑。一位拥有5年后端经验的开发者,三个月前启动了自己的“一人公司”——一个AI辅助写作SaaS,技术栈是Python Flask PostgreSQL React。本以为从本地迁移到云端就是几步操作的事,结果踩了一个又一个坑,硬生生折腾了好几周。以下是他踩坑后的复

时间:2026-07-07 15:44
阿里云DNS个人版19.9元/年 公网权威解析功能安全与续费说明

阿里云DNS个人版19.9元/年 公网权威解析功能安全与续费说明

许多用户最近都在咨询阿里云云解析DNS个人版的价格与续费问题,这里统一整理关键信息。个人版当前新用户优惠价仅为19 9元 年,但该优惠仅限首次购买,原价实际为48元 年。因此续费时若无额外折扣,将按48元 年计费。简单来说,首年19 9元即可入手,第二年恢复原价。0 云解析DNS个人版费用阿里云云

时间:2026-07-07 15:44
阿里云Qwen3.7-Max深度测评:极致推理与企业级部署落地指南

阿里云Qwen3.7-Max深度测评:极致推理与企业级部署落地指南

Qwen3 7-Max是阿里云百炼平台最新推出的旗舰级大模型,也是Qwen3 7系列中规模最大、综合能力最强的“顶配选手”。目前开放的是纯文本模型能力,但别被这个限制误导——它面向智能体时代设计,在编程、办公生产力、长周期自主执行等场景下,表现相当能打。推理能力、多模态理解、复杂任务处理都有显著升级

时间:2026-07-07 15:44
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 日榜
  • 周榜
  • 月榜