news 2026/8/28 6:44:10

Python实现混合搜索引擎:关键词检索与向量语义检索实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python实现混合搜索引擎:关键词检索与向量语义检索实战

1. 背景:为什么还需要一种“新类型”的搜索引擎

先看一个我们都很熟悉的场景:在传统的搜索引擎里输入“如何用Python做文本去重”,返回的结果往往是关键词匹配的页面集合,用户需要自己打开三到五个网页,把碎片化的答案拼凑成完整方案。这个过程看起来“能搜到”,但实际上搜索引擎并没有真正理解查询意图,只是在做字面匹配。

传统搜索引擎的核心是倒排索引加关键词匹配。你输入什么词,它就去找包含这些词或近似词的文档,再按相关性打分排序。这个机制在处理“事实型查询”时表现很好,比如“2024年春节是哪天”“Python list sort 用法”,但遇到下面几类需求时就显得吃力了:

  • 同一语义、不同表达的查询,例如“怎样给列表排序”和“list的sort方法怎么用”;
  • 长尾、个性化、上下文依赖的查询;
  • 需要综合多篇文档内容才能回答的问题;
  • 非文本内容的检索,比如图片、音频、代码片段。

所谓“新类型的搜索引擎”,本质上是把检索的单元从“关键词”升级为“语义”,把检索的目标从“返回网页链接”升级为“返回答案”甚至“生成答案”。这类系统通常结合了向量检索、语义模型、知识图谱和生成式AI,能够在召回阶段找出语义相近的内容,再在排序阶段用更精细的模型重排,最后可选地交给大语言模型组织成自然语言回答。

从事后端开发和架构设计的同学需要关注这个方向,因为传统的关键词搜索方案在数据量大、语义复杂时,已经很难满足业务要求。电商平台要搜索商品描述、企业内部要做文档问答、客服系统要检索历史工单,这些场景的核心都是“找得准”和“找得全”。掌握新型搜索引擎的基本原理和搭建方式,等于给自己的技术栈加了一项适应 AI 时代的基础能力。

本文将围绕“新型搜索引擎”展开,先拆解它的核心概念和关键技术点,再提供一个基于 Python 的轻量级可运行 Demo,演示如何把关键词检索与向量语义检索结合起来,实现一个简单的混合搜索服务。文章会尽量讲清楚每一步的“为什么”,也会列出常见问题和工程落地建议,适合对搜索技术、RAG 应用和 AI 工程化感兴趣的开发者。

2. 环境准备与版本说明

本文的实战示例是纯 Python 实现,重点演示搜索系统的原理和可运行性,不依赖重型搜索服务。建议在独立的虚拟环境中操作,避免依赖冲突。

环境/工具说明
操作系统Windows 10/11、macOS、Linux 均可
Python3.8 及以上版本,本文以 Python 3.10 为例
包管理pip
IDEPyCharm、VS Code 或命令行工具均可
额外依赖jieba、scikit-learn、flask

需要说明的是,不同操作系统和 Python 版本下,第三方库的安装方式基本一致。如果你的环境是 Python 3.12 或更高版本,建议优先安装最新稳定版依赖。版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。

准备项目目录,建议命名为semantic_search_demo,后续所有代码都放在这个目录下:

mkdir semantic_search_demo cd semantic_search_demo python -m venv venv

激活虚拟环境:

# Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate

安装依赖:

pip install jieba scikit-learn flask

安装完成后,可以快速验证环境是否正常:

python -c "import jieba; import sklearn; import flask; print('deps ok')"

如果看到deps ok,说明基础环境已经就绪。

3. 核心原理:从倒排索引到向量检索

在写代码之前,先建立一套“搜索引擎技术地图”。这样后面看代码时,就不会觉得每个函数都是孤立的。

3.1 传统搜索引擎的核心:倒排索引

传统搜索引擎通常会建立倒排索引。简单来说,倒排索引是“词 -> 文档列表”的映射。比如有三篇文档:

  • 文档A:“Python列表排序”
  • 文档B:“Java数组排序”
  • 文档C:“机器学习基础”

分词后建立索引,大致会形成一个结构:

文档
PythonA
列表A
排序A、B
JavaB
数组B
机器学习C
基础C

用户搜索“排序”时,系统能迅速定位到文档 A 和 B。

倒排索引的优势是查询速度快、实现简单、资源消耗可控,但它存在一个明显的短板:词与词之间的语义关系没有被建模。用户搜“Python 列表排序”,如果某篇文档写的是“list sort 用法”,而分词后没有“排序”这个词,那这篇文档就不会被召回。

3.2 新型搜索引擎的核心:向量语义检索

向量语义检索的思路是把文本映射到一个高维向量空间中,让语义相近的文本在向量空间中距离更近。比如“怎么给列表排序”和“list sort 方法”虽然字面差异很大,但语义向量之间的余弦相似度会比较高。

这个“文本 -> 向量”的过程称为向量化。实现向量化的方式有很多种:

  • 基于词频统计的 TF-IDF 向量和 BM25 向量;
  • 基于词向量的 Word2Vec、GloVe,再聚合得到句子向量;
  • 基于预训练语言模型的向量,如 Sentence-BERT 等。

在实际的“新型搜索引擎”架构中,通常会使用专门的向量数据库(如 Milvus、Weaviate、Qdrant)或搜索引擎的向量能力(如 Elasticsearch 的 dense_vector)来存储向量、计算相似度。但在本文的示例中,为了降低搭建门槛,我会用 scikit-learn 的 TF-IDF 向量化器配合余弦相似度来演示,后续可以平滑替换成更高级的模型。

需要注意的是,TF-IDF 向量不是真正的“语义向量”,它仍然是基于词汇共现的稀疏表示。但它已经能处理一部分同义词问题,并且非常适合作为新手理解向量检索的起点。真正的语义向量需要加载预训练模型,门槛更高,原理上依然是“向量化 + 相似度计算”。

3.3 混合搜索与重排序

关键词检索和向量检索各有优缺点:

检索方式优势劣势
关键词检索(稀疏)精确匹配好,能处理专有名词、型号、代码无法理解语义,容易漏召回
向量检索(稠密)能处理语义相似,支持模糊表达对精确数字、ID、专有名词召回不稳定

所以工业界经常采用混合搜索:先用关键词和向量分别召回一批候选文档,再用重排序模型对候选结果进行精细化打分,最后输出排序结果。这种“召回-排序”两阶段架构,是目前新型搜索引擎的主流设计。

重排序的输入是查询和候选文档的配对特征,输出是相关性分数。简单场景下,可以用加权融合的方式代替模型重排序,这也是本文 Demo 的做法。

3.4 RAG:搜索引擎与生成式 AI 的结合

RAG(Retrieval-Augmented Generation,检索增强生成)是当前最热门的新型搜索落地形态。它的思路是:用户提问后,先从知识库中检索相关文档片段,再把文档片段作为上下文喂给大语言模型,由模型生成自然语言答案。

RAG 解决了两个问题:

  • 大模型内部知识有截止时间,无法回答企业私有数据的问题;
  • 大模型可能产生幻觉,而检索出来的文档可以作为事实依据。

RAG 系统里,检索模块的好坏直接决定最终答案的质量。即使生成模型再强,如果检索阶段没有拿到相关上下文,答案也只能靠“编”。因此,新型搜索引擎的技术核心仍然在检索环节。

4. 完整实战:使用 Python 实现一个混合搜索 Demo

下面进入实战环节。我们会实现一个简单的文档搜索服务,支持关键词检索和向量检索两种模式,并将二者结果做加权融合,最终通过 Flask 提供 HTTP 接口。

4.1 创建项目结构

semantic_search_demo/ ├── corpus.py # 文档数据 ├── keyword_search.py # 关键词检索模块 ├── vector_search.py # 向量检索模块 ├── hybrid_search.py # 混合检索模块 ├── app.py # Flask 服务 └── requirements.txt # 依赖清单

先创建requirements.txt

jieba scikit-learn flask

安装依赖:

pip install -r requirements.txt

4.2 准备文档数据

创建一个简单的文档集合。这里故意加入语义相似但字面差异明显的中文文档,便于测试语义检索效果。

# 文件路径:semantic_search_demo/corpus.py documents = [ "Python 列表的 sort 方法可以按升序对列表元素进行排序。", "在 Python 中,使用 sorted 函数可以得到一个新的已排序列表。", "Java 语言中可以使用 Collections.sort 对集合进行排序。", "机器学习是人工智能的一个分支,主要研究如何让计算机从数据中学习。", "深度学习是机器学习的一个子领域,使用多层神经网络进行训练。", "今天北京天气晴朗,气温 25 摄氏度。", "MySQL 索引可以加快查询速度,但会占用额外的存储空间。", "Redis 是一种基于内存的键值存储系统,常被用作缓存组件。", ] # 给每篇文档一个 ID corpus = {index: text for index, text in enumerate(documents)}

这个语料库只有 8 条数据,但对演示来说已经足够。你可以看到,第 0、1、2 条都跟“排序”相关,但表达差异很大;第 3、4 条是机器学习相关;第 6、7 条是后端技术相关。后面测试时,可以用不同风格的查询来验证“混合检索”的优势。

4.3 关键词检索模块

关键词检索模块基于 jieba 分词和词频统计,实现类似“简化版 BM25”的效果。这里先演示最直观的“词频-逆文档频率”加权,后续可替换为更成熟的分词检索方案。

# 文件路径:semantic_search_demo/keyword_search.py import math from collections import Counter import jieba from corpus import corpus class KeywordSearch: def __init__(self, corpus_dict): self.corpus = corpus_dict self.doc_count = len(corpus_dict) # 预计算每个词的逆文档频率 IDF self.idf = self._compute_idf() def _tokenize(self, text): return [word for word in jieba.cut(text) if word.strip()] def _compute_idf(self): doc_freq = {} for text in self.corpus.values(): word_set = set(self._tokenize(text)) for word in word_set: doc_freq[word] = doc_freq.get(word, 0) + 1 idf = {} for word, freq in doc_freq.items(): idf[word] = math.log((self.doc_count + 1) / (freq + 1)) + 1 return idf def search(self, query, top_k=3): query_words = self._tokenize(query) if not query_words: return [] scores = [] for doc_id, text in self.corpus.items(): doc_words = self._tokenize(text) if not doc_words: continue word_count = Counter(doc_words) total_words = len(doc_words) score = 0.0 for word in query_words: # 词频 TF tf = word_count.get(word, 0) / total_words # TF-IDF 加权 score += tf * self.idf.get(word, 0) scores.append((doc_id, score)) # 按分数降序排序 scores.sort(key=lambda x: x[1], reverse=True) return scores[:top_k]

这里的核心思想是:

  • 对文档和查询都进行分词。
  • 统计查询词在文档中的出现频率,并使用 IDF 提升区分度。
  • 最终得分是多个查询词加权得分之和。

可以看到,如果查询中出现的词不会出现在任何文档中,那这个模块基本拿不到好的召回结果。这正是向量检索模块要弥补的地方。

4.4 向量检索模块

向量检索模块使用 scikit-learn 的TfidfVectorizer配合 jieba 分词,把文档和查询文本映射成 TF-IDF 向量,再用余弦相似度计算相似度。

# 文件路径:semantic_search_demo/vector_search.py from sklearn.feature_extraction.text import TfidfVectorizer import jieba from corpus import corpus def tokenize_chinese(text): # jieba 分词,并过滤空字符 return " ".join([w for w in jieba.cut(text) if w.strip()]) class VectorSearch: def __init__(self, corpus_dict): self.corpus = corpus_dict self.doc_ids = list(corpus_dict.keys()) self.doc_texts = [tokenize_chinese(text) for text in corpus_dict.values()] # 构建 TF-IDF 向量化器 self.vectorizer = TfidfVectorizer(tokenizer=lambda x: x.split()) self.doc_matrix = self.vectorizer.fit_transform(self.doc_texts) def _cosine_similarity(self, query_vec): # 余弦相似度 = 向量点积 / (模的乘积) # 由于 TF-IDF 矩阵是稀疏矩阵,使用 sklearn 内置的余弦相似度比较简单 from sklearn.metrics.pairwise import cosine_similarity return cosine_similarity(query_vec, self.doc_matrix).flatten() def search(self, query, top_k=3): query_text = tokenize_chinese(query) query_vec = self.vectorizer.transform([query_text]) scores = self._cosine_similarity(query_vec) results = [] for idx, score in enumerate(scores): results.append((self.doc_ids[idx], float(score))) results.sort(key=lambda x: x[1], reverse=True) return results[:top_k]

这个模块的流程是:

  1. 把原始文档分段并用 jieba 分词,分词结果以空格连接。
  2. TfidfVectorizer拟合文档集合,得到每条文档的向量表示。
  3. 查询时,对查询文本做同样的分词处理,并转换成向量。
  4. 使用余弦相似度计算查询与所有文档之间的相似度。

TF-IDF 向量虽然仍是稀疏向量,但它为“向量检索”提供了非常直观的示例:你有一组向量,查询也被转成向量,然后通过相似度计算找最近邻。将来引入预训练向量模型时,整体代码结构基本不变,只需要替换向量化器即可。

4.5 混合检索模块

混合检索模块把关键词得分和向量相似度得分做归一化后加权融合。因为关键词得分和向量相似度得分的量纲不同,必须先做 min-max 归一化。

# 文件路径:semantic_search_demo/hybrid_search.py from keyword_search import KeywordSearch from vector_search import VectorSearch from corpus import corpus def normalize_scores(result_list): """把 [(doc_id, score)] 归一化到 [0,1] 区间""" if not result_list: return [] scores = [score for _, score in result_list] min_score = min(scores) max_score = max(scores) diff = max_score - min_score if diff == 0: return [(doc_id, 1.0) for doc_id, _ in result_list] return [(doc_id, (score - min_score) / diff) for doc_id, score in result_list] class HybridSearch: def __init__(self, corpus_dict, keyword_weight=0.4, vector_weight=0.6): self.keyword_engine = KeywordSearch(corpus_dict) self.vector_engine = VectorSearch(corpus_dict) self.keyword_weight = keyword_weight self.vector_weight = vector_weight def search(self, query, top_k=3): kw_results = normalize_scores(self.keyword_engine.search(query, top_k=len(self.keyword_engine.corpus))) vec_results = normalize_scores(self.vector_engine.search(query, top_k=len(self.vector_engine.corpus))) score_map = {} for doc_id, score in kw_results: score_map[doc_id] = score_map.get(doc_id, 0) + self.keyword_weight * score for doc_id, score in vec_results: score_map[doc_id] = score_map.get(doc_id, 0) + self.vector_weight * score sorted_results = sorted(score_map.items(), key=lambda x: x[1], reverse=True) return sorted_results[:top_k]

混合检索的策略解释:

  • 关键词检索负责精确匹配,对专有名词、型号、代码片段更友好。
  • 向量检索负责语义召回,能弥补关键词遗漏。
  • 加权融合时,可以调整keyword_weightvector_weight。如果业务数据偏技术文档,关键词权重可以略高;如果数据偏口语化问答,向量权重可以加大。

4.6 使用 Flask 提供搜索接口

接下来用 Flask 封装一个简单的 HTTP 服务,便于在浏览器中测试。

# 文件路径:semantic_search_demo/app.py from flask import Flask, request, jsonify from hybrid_search import HybridSearch from corpus import corpus, documents app = Flask(__name__) search_engine = HybridSearch(corpus, keyword_weight=0.4, vector_weight=0.6) @app.route("/search", methods=["GET"]) def search_endpoint(): query = request.args.get("q", "") if not query.strip(): return jsonify({"error": "query is required"}), 400 top_k = request.args.get("top_k", default=3, type=int) results = search_engine.search(query, top_k=top_k) output = [] for doc_id, score in results: output.append({ "doc_id": doc_id, "score": round(score, 4), "text": documents[doc_id] }) return jsonify({"query": query, "results": output}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)

启动服务:

python app.py

启动后,在浏览器访问:

http://127.0.0.1:5000/search?q=怎么给列表排序

预期会返回一组与“排序”相关的文档,并且排序结果会综合关键词召回和向量召回。为了对比效果,你可以分别测试:

  • q=列表排序
  • q=使用排序函数
  • q=机器学习包含哪些分支
  • q=缓存组件
  • q=天气

通过对比结果你会发现:

  • 当查询词本身比较完整时,关键词检索和向量检索的结果都比较好。
  • 当查询词换了一种说法时,关键词检索可能漏掉某些文档,而向量检索能把语义相近的文档捞回来。
  • 混合检索会让 Top 结果更稳定。

5. 常见问题与排查思路

下面是这个 Demo 从环境搭建到运行过程中可能遇到的典型问题,以及对应的排查思路。

问题现象常见原因解决思路
导入 jieba 时报错 ModuleNotFoundError没有安装依赖或虚拟环境未激活检查虚拟环境是否激活,执行pip install jieba
中文分词效果差jieba 默认词典不包含领域专有词使用jieba.load_userdict加载自定义词典
向量检索结果全为 0查询词在所有文档中都没出现,TF-IDF 向量为零向量切换语义向量模型,或补充同义词扩展
混合检索返回结果不理想关键词与向量权重设置不合理根据业务场景调整keyword_weightvector_weight
文档数量增多后检索变慢每次查询都做全量相似度计算引入向量数据库或倒排索引,使用 ANN 检索
部署到服务器后访问不了Flask 默认绑定 127.0.0.1,或防火墙未放行修改 host 为 0.0.0.0,检查云安全组规则
接口被频繁调用时响应慢Flask 开发服务器不适合高并发使用 Gunicorn/uvicorn 部署,或引入缓存层

在实际项目中,最常见的性能瓶颈不是模型本身,而是全量扫描式的相似度计算。当文档量级到达百万级别时,必须换用支持 ANN(近似最近邻)的向量数据库。这是从 Demo 走向生产环境的关键一步。

6. 最佳实践与工程建议

6.1 数据清洗比模型更重要

很多人一上来就选模型、调权重,却忽略了数据质量。搜索系统的上限取决于文档数据的干净程度,而不是模型复杂度。

建议在建立索引之前完成以下清洗工作:

  • 去除 HTML 标签、超链接、乱码字符;
  • 统一全角半角符号;
  • 合并重复或近似重复的文档;
  • 对较长文档进行段落切分,切分粒度要兼顾上下文完整性和检索精度;
  • 对结构化字段(作者、时间、分类)单独建字段,不要混在正文里。

如果数据本身乱七八糟,任何精妙算法都会被噪音淹没。

6.2 索引与分片策略

在生产环境,文档要写入搜索引擎或向量数据库。建议把“原始文档”和“索引数据”分开存储,便于重建索引。索引 Schema 设计要尽量覆盖业务检索维度,例如:

  • 标题字段(title):提升标题匹配权重;
  • 正文字段(content):用于召回和排序;
  • 标签字段(tags):支持结构化筛选;
  • 时间字段(created_at):支持时间范围过滤。

如果是向量检索,还需要考虑向量维度、距离度量方式和分片策略。向量维度需要与所选模型匹配,距离度量一般选择余弦相似度或内积。

6.3 检索效果评估

搜索系统上线前,一定要建立一套小规模的评估集。具体做法是准备若干条代表性查询,并为每条查询标注正确的文档 ID 集合。上线后使用 MRR(Mean Reciprocal Rank)或 Recall@K 指标跟踪效果。

例如,对查询“列表排序”,人工判定文档 0、1、2 是相关文档。如果系统返回的第一条结果就在相关集合内,MRR 得分就高;如果返回了不相关内容,说明检索链路有问题。通过积累评估集,你可以在修改算法时快速判断是否“变好了”还是“变差了”。

6.4 安全与权限

搜索接口是典型的对外入口,需要注意:

  • 对输入参数做合法性校验,限制top_k的取值,避免过大请求造成资源消耗;
  • 权限控制要落到文档级别,用户只能搜索到有权限查看的文档,避免越权访问;
  • 记录搜索日志,但要对查询内容做脱敏处理,防止敏感信息泄露;
  • 如果接入大语言模型,要考虑提示词注入风险:文档内容可能夹带恶意指令,需要在调用模型前做好上下文过滤。

6.5 性能优化

从 Demo 到生产,搜索性能通常需要从三个层面优化:

  • 召回层:使用倒排索引或 ANN 索引,避免全表扫描;
  • 缓存层:对热门查询结果做短时缓存,降低重复计算压力;
  • 排序层:重排序模型只对 Top 候选集生效,不要对全部文档做复杂模型推理。

监控方面,需要关注 P99 延迟、召回率、吞吐量等核心指标。建议在服务启动时打印索引文档数量、向量维度、内存占用等基础信息,便于排查问题。

6.6 最小权限与灰度发布

如果搜索服务涉及线上配置变更或索引重建,一定要遵循最小权限原则,先在测试环境验证,再灰度发布。尤其是模型版本升级、分词词典更新、权重调整这类操作,很可能对线上结果产生不可控影响。建议用影子流量或 A/B 方式验证新旧版本的效果差异,确认无回退后再全量切换。

7. 下一步可以怎么走

到这里,你已经从一个传统的关键词搜索概念,逐步走到向量检索、混合搜索以及 RAG 的整体技术脉络,并且亲手实现了可运行的搜索 Demo。这个 Demo 虽然只有几百行代码,但它包含了现代搜索引擎的两个核心环节:召回和排序,也揭示了“混合检索”的基本工作方式。

接下来可以往这几个方向深入:

  • 把 TF-IDF 向量替换为预训练语义向量模型,观察语义召回能力的提升;
  • 引入向量数据库,解决大规模向量检索的性能问题;
  • 在混合检索后增加基于机器学习模型的重排序层;
  • 把搜索模块适配到 RAG 系统,接入大模型生成答案;
  • 增加同义词扩展、查询改写、用户点击反馈等模块,进一步优化搜索效果。

结合我自己的落地经验,真正把“新型搜索引擎”做到好用,最耗费精力的往往不是算法,而是数据治理、效果评估和系统稳定性。建议你在学习算法原理的同时,尽早建立评估集和监控体系。这样每次改动,你都能清楚地知道是变好了还是变差了,而不是凭感觉调参。

希望这篇文章能帮你建立起对新型搜索引擎的完整认知。如果你在动手实现时遇到问题,欢迎按文中的排查思路一步步定位,也可以把问题记录下来作为自己的排错手册。动手试一遍,比看十遍原理更有收获。

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

控制系统Matlab仿真:数学模型建立与Simulink实现全解析

1. 项目概述:从理论到实践的桥梁搞控制系统,尤其是自动控制、机器人或者机电一体化方向的工程师和学生,估计都绕不开一个环节:仿真。而一提到仿真,Matlab/Simulink几乎是我们的“第二工作台”。但不知道你有没有过这样…

作者头像 李华
网站建设 2026/8/28 6:43:43

AI Agent文档层设计:DocuQueue如何构建可检索的RAG异步管道

AI Agent 能调模型、能写代码、能操作工具,但一遇到企业内部 PDF、Word、Excel 组成的长文档,问题就出来了:上下文窗口放不下,文件格式又杂,文档还在不断更新。这个问题不能靠 prompt 修补,需要一个专门的文…

作者头像 李华
网站建设 2026/8/28 6:43:00

AI对冲基金濒临崩盘:自动化决策如何用四道闸门防失控

AI 进入金融交易的时间并不算短,但真正让人后背发凉的,是它开始自己下决定之后的那一瞬间。最近,一只名为 Situational Awareness 的 AI 对冲基金被曝险些崩盘,并且正在遭受 SEC 调查。标题里的几个词放到一起,几乎戳中…

作者头像 李华
网站建设 2026/8/28 6:42:53

使用codexpro实现网页端chatgpt操作本地项目

1.安装codexpro MCP https://github.com/rebel0789/codexpro/blob/main/README_ZH.md 先按照md启动起来,配置网页端插件,新增插件名称codexpro,url填写启动的cmd最后的url,认证方式选none,初步能用以后,由于…

作者头像 李华
网站建设 2026/8/28 6:42:17

手写数字识别毕业设计:从CNN模型到论文答辩的全流程实战指南

简介:卷积神经网络(CNN)作为深度学习在计算机视觉领域的核心技术,通过卷积、池化等操作自动提取图像特征,实现了从原始像素到高级语义的端到端学习。其核心价值在于解决了传统方法中手工特征设计的复杂性与局限性&…

作者头像 李华
网站建设 2026/8/28 6:41:10

基于SpringBoot的美食信息推荐网站系统毕业设计项目源码

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

作者头像 李华