Lucene 全文搜索 引擎 工具类

Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供。适用于小型项目的全文检索需求,不需要进行单独的服务部署,相对于elasticsearch 轻盈方便快捷实用。

以下代码亲测有效实用

工具类中涉及的依赖库可自行导入。

核心代码如下


import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TermRangeQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.highlight.Fragmenter;
import org.apache.lucene.search.highlight.Highlighter;
import org.apache.lucene.search.highlight.QueryScorer;
import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
import org.apache.lucene.search.highlight.SimpleSpanFragmenter;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.BytesRef;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.StringReader;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;

/**
 * <p>
 * Lucene工具类
 * <p>
 * Lucene的主要组件包括:
 * IndexWriter: 用于创建索引的主要类。它将文档加入索引,可以从索引中删除文档,也可以更新索引。
 * Directory: 索引的存储方式。Lucene允许索引存储在几种不同的地方,如磁盘、内存等。
 * Analyzer: 文本分析器,用于分析文本以产生索引项。(这个是重点)
 * Document: 一个包含各种字段的容器,这些字段最终被建入索引。
 * Field: 文档的一个组成部分,包含了一个名字和一串值。
 * IndexSearcher: 用于执行搜索查询并返回查询结果的类。
 * Query: 查询对象,代表用户的搜索查询条件。
 * QueryParser: 用于解析用户输入的查询字符串并生成相应的Query对象。
 * ScoreDocs: 一个Searcher.search()方法的结果,它包含了与查询条件相匹配的文档,以及它们的相关度分数。
 * Term: 索引中的最小单位,表示文档的一个特定词。
 * <p>
 * 联合查询
 * Query query1 = new TermQuery(new Term("", ""));
 * Query query2 = new TermQuery(new Term("", ""));
 * BooleanClause booleanClause1=new BooleanClause(query1, BooleanClause.Occur.MUST);
 * BooleanClause booleanClause2=new BooleanClause(query1, BooleanClause.Occur.SHOULD);
 * BooleanClause booleanClause2=new BooleanClause(query1, BooleanClause.Occur.MUST_NOT);
 * <p>
 * BooleanQuery booleanQuery = new BooleanQuery.Builder().add(booleanClause1).add(booleanClause1).build();
 * searcher.search(booleanQuery,num)
 * <p>
 * <p>
 * 分页的实现  searcher.search(query, num) num按多查询,后手动分页返回
 * 或 searcher.searchAfter() 与记录当前已取得最新的文档联合查询
 * </p>
 *
 * @author 
 * @since 2024/5/22 15:21
 */
@Slf4j
@Component
public final class ZLuceneUtil {

    @Value("${lucene.index-path-unix}")
    String indexPathUnix;
    @Value("${lucene.index-path-windows}")
    String indexPathWindows;

    /**
     * 获取标准分词器的写索引实例
     *
     * @return 标准分词器的写索引实例
     *
     * @throws Exception 异常
     */
    public IndexWriter indexer() throws Exception {
        String indexPath = ZOSUtil.isWindows() ? indexPathWindows : indexPathUnix;
        Directory dir = FSDirectory.open(Paths.get(indexPath));
        //标准分词器,会自动去掉空格啊,is a the等单词
        Analyzer analyzer = new StandardAnalyzer();
        //将标准分词器配到写索引的配置中
        IndexWriterConfig config = new IndexWriterConfig(analyzer);
        //实例化写索引对象
        IndexWriter writer;
        if (StrUtil.isEmpty(indexPath)) {
            writer = new IndexWriter(new ByteBuffersDirectory(), config);
        } else {
            writer = new IndexWriter(dir, config);
        }
        return writer;
    }

    public void writerClose(IndexWriter indexWriter) throws Exception {
        indexWriter.close();
    }

    /**
     * 删除索引,根据文档主键
     *
     * @throws Exception 异常
     */
    public void deleteIndex(String id) throws Exception {
        IndexWriter indexWriter = indexer();
        Query query = new TermQuery(new Term(TextFieldConstant.FILE_ID, id));
        indexWriter.deleteDocuments(query);
        indexWriter.commit();
        writerClose(indexWriter);
    }

    /**
     * 获取中文分词器的写索引实例
     *
     * @param indexDir 索引存储路径,null为内存方式否则为磁盘方式
     *
     * @return 中文分词器的写索引实例
     *
     * @throws Exception 异常
     */
    public IndexWriter indexerChinese(String indexDir) throws Exception {
        Directory dir = FSDirectory.open(Paths.get(indexDir));
        //标准分词器,会自动去掉空格啊,is a the等单词
        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
        //将标准分词器配到写索引的配置中
        IndexWriterConfig config = new IndexWriterConfig(analyzer);
        //实例化写索引对象
        IndexWriter writer;
        if (StrUtil.isEmpty(indexDir)) {
            writer = new IndexWriter(new ByteBuffersDirectory(), config);
        } else {
            writer = new IndexWriter(dir, config);
        }
        return writer;
    }

    /**
     * 索引指定的文件
     *
     * @param file 文件
     *
     * @throws Exception 异常
     */
    public void indexFile(UploadFileInfo file, String docId) throws Exception {
        //调用下面的getDment方法,获取该文件的document
        IndexWriter indexWriter = indexer();
        deleteIndex(docId);
        Document doc = getDocument(file, docId);
        //将doc添加到索引中
        indexWriter.addDocument(doc);
        indexWriter.commit();
        writerClose(indexWriter);
    }

    /**
     * 获取文档,文档里再设置每个字段,就类似于数据库中的一行记录
     *
     * @param file 文件
     *
     * @return 文档
     *
     * @throws Exception 异常
     */
    public Document getDocument(UploadFileInfo file, String docId) throws Exception {
        Document doc = new Document();
        String content = FileUtils.getFileContent(file.getAbsolutePath());
        //开始添加字段
        //添加内容
        doc.add(new TextField(TextFieldConstant.CONTENT, content, Field.Store.YES));
        //添加文件名,并把这个字段存到索引文件里
        doc.add(new TextField(TextFieldConstant.FILE_NAME, file.getOriginFileName(), Field.Store.YES));
        //添加文件路径
        doc.add(new TextField(TextFieldConstant.FILE_PATH, file.getAbsolutePath(), Field.Store.YES));
        //添加文件类型
        doc.add(new TextField(TextFieldConstant.FILE_TYPE, file.getFileType(), Field.Store.YES));
        //添加文件id
        doc.add(new TextField(TextFieldConstant.FILE_ID, docId, Field.Store.YES));
        return doc;
    }

    /**
     * 基础查询 默认
     *
     * @param indexDir           索引路径
     * @param queryStr           查询的字符串
     * @param queryTextFieldName 查询文档字段名
     * @param analyzer           分词器
     * @param num                返回的文档个数
     *
     * @return 文档列表
     */
    public List<Document> query(String indexDir, String queryStr, String queryTextFieldName, Analyzer analyzer,
            int num) {
        List<Document> result = new ArrayList<>();
        try {
            //获取要查询的路径,也就是索引所在的位置
            Directory dir = FSDirectory.open(Paths.get(indexDir));
            IndexReader reader = DirectoryReader.open(dir);
            //构建IndexSearcher
            IndexSearcher searcher = new IndexSearcher(reader);
            //查询解析器
            QueryParser parser = new QueryParser(queryTextFieldName, analyzer);
            //通过解析要查询的String,获取查询对象,queryStr为传进来的待查的字符串
            Query query = parser.parse(queryStr);
            //开始查询,查询前num条数据,将记录保存在docs中
            TopDocs docs = searcher.search(query, num);
            //取出每条查询结果
            for (ScoreDoc scoreDoc : docs.scoreDocs) {
                //scoreDoc.doc相当于docID,根据这个docID来获取文档
                Document doc = searcher.doc(scoreDoc.doc);
                result.add(doc);
            }
            reader.close();
        } catch (Exception e) {
            log.error(ThrowableUtils.extractStackTrace(e));
        }
        return result;
    }

    /**
     * 段域查询
     *
     * @param indexDir           索引路径
     * @param startStr           开始字符
     * @param endStr             结束字符
     * @param queryTextFieldName 查询字段
     * @param num                返回文档数量
     * @param includeLower       是否包括开始字符
     * @param includeUpper       是否包括结束字符
     *
     * @return 文档列表
     */
    public List<Document> query(String indexDir, String startStr, String endStr, String queryTextFieldName, int num,
            boolean includeLower, boolean includeUpper) {
        List<Document> result = new ArrayList<>();
        try {
            //获取要查询的路径,也就是索引所在的位置
            Directory dir = FSDirectory.open(Paths.get(indexDir));
            IndexReader reader = DirectoryReader.open(dir);
            //构建IndexSearcher
            IndexSearcher searcher = new IndexSearcher(reader);
            //查询解析器
            Query query = new TermRangeQuery(queryTextFieldName,
                                             new BytesRef(startStr.getBytes()),
                                             new BytesRef(endStr.getBytes()),
                                             includeLower,
                                             includeUpper);
            TopDocs docs = searcher.search(query, num);
            //取出每条查询结果
            for (ScoreDoc scoreDoc : docs.scoreDocs) {
                //scoreDoc.doc相当于docID,根据这个docID来获取文档
                Document doc = searcher.doc(scoreDoc.doc);
                result.add(doc);
            }
            reader.close();
        } catch (Exception e) {
            log.error(ThrowableUtils.extractStackTrace(e));
        }
        return result;
    }

    /**
     * 前缀查询
     *
     * @param indexDir           索引路径
     * @param queryStr           查询字符串
     * @param queryTextFieldName 查询字段
     * @param num                返回文档数量
     *
     * @return 文档列表
     */
    public List<Document> query(String indexDir, String queryStr, String queryTextFieldName, int num) {
        List<Document> result = new ArrayList<>();
        try {
            //获取要查询的路径,也就是索引所在的位置
            Directory dir = FSDirectory.open(Paths.get(indexDir));
            IndexReader reader = DirectoryReader.open(dir);
            //构建IndexSearcher
            IndexSearcher searcher = new IndexSearcher(reader);
            //查询解析器
            Query query = new PrefixQuery(new Term(queryTextFieldName, queryStr));
            TopDocs docs = searcher.search(query, num);
            //取出每条查询结果
            for (ScoreDoc scoreDoc : docs.scoreDocs) {
                //scoreDoc.doc相当于docID,根据这个docID来获取文档
                Document doc = searcher.doc(scoreDoc.doc);
                result.add(doc);
            }
            reader.close();
        } catch (Exception e) {
            log.error(ThrowableUtils.extractStackTrace(e));
        }
        return result;
    }

    /**
     * 模糊查询
     *
     * @param queryStr 查询字符串
     *
     * @return 文档主键列表
     */
    public Map<String, String> queryLike(Integer page, Integer size, String queryStr) {
        Map<String, String> result = new HashMap<>();
        try {
            String indexPath = ZOSUtil.isWindows() ? indexPathWindows : indexPathUnix;
            //获取要查询的路径,也就是索引所在的位置
            Directory dir = FSDirectory.open(Paths.get(indexPath));
            IndexReader reader = DirectoryReader.open(dir);
            //构建IndexSearcher
            IndexSearcher searcher = new IndexSearcher(reader);
            //查询解析器
            Query query = new FuzzyQuery(new Term(TextFieldConstant.CONTENT, queryStr));
            TopDocs docs = searcher.search(query, 1000);
            //取出每条查询结果
            if (ObjectUtil.isNotNull(docs)) {
                List<ScoreDoc> scoreDocList = ZPageUtil.splitList(page, size, Arrays.asList(docs.scoreDocs.clone()));
                for (ScoreDoc scoreDoc : scoreDocList) {
                    //scoreDoc.doc相当于docID,根据这个docID来获取文档
                    Document doc = searcher.doc(scoreDoc.doc);
                    result.put(doc.getField(TextFieldConstant.FILE_ID).stringValue(), highlighter(doc, query));
                }
            }

            reader.close();
        } catch (Exception e) {
            log.error(ThrowableUtils.extractStackTrace(e));
        }
        return result;
    }

    /**
     * 内容高亮
     */
    public String highlighter(Document document, Query query) {
        //取出每条查询结果
        //如果不指定参数的话,默认是加粗,即<b><b/>
        SimpleHTMLFormatter simpleHtmlFormatter = new SimpleHTMLFormatter("<b><font color=red>", "</font></b>");
        //根据查询对象计算得分,会初始化一个查询结果最高的得分
        QueryScorer scorer = new QueryScorer(query);
        //根据这个得分计算出一个片段
        Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
        //将这个片段中的关键字用上面初始化好的高亮格式高亮
        Highlighter highlighter = new Highlighter(simpleHtmlFormatter, scorer);
        //设置一下要显示的片段
        highlighter.setTextFragmenter(fragmenter);
        try {
            String desc = document.get(TextFieldConstant.CONTENT);
            //显示高亮
            if (desc != null) {
                Analyzer analyzer = new StandardAnalyzer();
                TokenStream tokenStream = analyzer.tokenStream(TextFieldConstant.CONTENT, new StringReader(desc));
                return highlighter.getBestFragment(tokenStream, desc);
            }

        } catch (Exception e) {
            log.debug(ThrowableUtils.extractStackTrace(e));
        }
        return document.getField(TextFieldConstant.CONTENT).stringValue();
    }

    /**
     * 返回文档的内容做高亮 加红加粗处理
     *
     * @param documents 文档列表
     * @param analyzer  分词器
     * @param query     查询对象
     *
     * @return 文档的内容做高亮 加红加粗处理
     */
    public List<String> highlighter(List<Document> documents, Analyzer analyzer, Query query) {
        //取出每条查询结果
        List<String> list = new ArrayList<>();
        //如果不指定参数的话,默认是加粗,即<b><b/>
        SimpleHTMLFormatter simpleHtmlFormatter = new SimpleHTMLFormatter("<b><font color=red>", "</font></b>");
        //根据查询对象计算得分,会初始化一个查询结果最高的得分
        QueryScorer scorer = new QueryScorer(query);
        //根据这个得分计算出一个片段
        Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
        //将这个片段中的关键字用上面初始化好的高亮格式高亮
        Highlighter highlighter = new Highlighter(simpleHtmlFormatter, scorer);
        //设置一下要显示的片段
        highlighter.setTextFragmenter(fragmenter);
        try {
            for (Document doc : documents) {
                //scoreDoc.doc相当于docID,根据这个docID来获取文档
                String desc = doc.get(TextFieldConstant.CONTENT);
                //显示高亮
                if (desc != null) {
                    TokenStream tokenStream = analyzer.tokenStream(TextFieldConstant.CONTENT, new StringReader(desc));
                    String summary = highlighter.getBestFragment(tokenStream, desc);
                    list.add(summary);
                }
            }
        } catch (Exception e) {
            log.debug(ThrowableUtils.extractStackTrace(e));
        }
        return list;
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/755692.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Qt】之【Bug】大量出现“未定义的标识符”问题

背景 构建时出现大量错误 原因 中文注释问题 解决 方法1. 报错代码附近的中文注释全部删掉。。。 方法2. 报错的文件添加 // Chinese word comment solution #pragma execution_character_set("utf-8")

爱奇艺 Opal 机器学习平台:特征中心建设实践

01 综述 Opal 是爱奇艺大数据团队研发的一站式机器学习平台&#xff0c;旨在提升特征迭代、模型训练效率&#xff0c;帮助业务提高收益。整个平台覆盖了机器学习生命周期中特征生产、样本构建、模型探索、模型训练、模型部署等在内的多个关键环节。其中特征作为模型训练的基石…

ZYNQ MPSOC浅说

1 MPSOC PL端 Zynq UltraScale MPSoC PL 部分等价于 FPGA。简化的 FPGA 基本结构由 6 部分组成&#xff0c;分别为可编程输入/输出单元、基本可编程逻辑单元、嵌入式块RAM、丰富的布线资源、底层嵌入功能单元和内嵌专用硬核等。 2 MPSOC PS端 MPSoC 实际上是一个以处理器为…

Quartz定时任务组件

官网&#xff1a;http://www.quartz-scheduler.org/ 1&#xff09;job - 任务 - 你要做什么事&#xff1f; 2&#xff09;Trigger - 触发器 - 做什么事&#xff0c;什么时候触发&#xff0c;可以传入任务 3&#xff09;Scheduler - 任务调度 - 可以传入多个触发器进行任务调…

软件测试之接口测试(Postman/Jmeter)

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、什么是接口测试 通常做的接口测试指的是系统对外的接口&#xff0c;比如你需要从别的系统来…

动手学深度学习(Pytorch版)代码实践 -卷积神经网络-29残差网络ResNet

29残差网络ResNet import torch from torch import nn from torch.nn import functional as F import liliPytorch as lp import matplotlib.pyplot as plt# 定义一个继承自nn.Module的残差块类 class Residual(nn.Module):def __init__(self, input_channels, num_chan…

AI副业赚钱攻略:掌握数字时代的机会

前言 最近国产大模型纷纷上线&#xff0c;飞入寻常百姓家。AI副业正成为许多人寻找额外收入的途径。无论您是想提高家庭收入还是寻求职业发展&#xff0c;这里有一个变现&#xff0c;帮助您掌握AI兼职副业的机会。 1. 了解AI的基础知识 在开始之前&#xff0c;了解AI的基础…

【笔记】Spring Cloud Gateway 实现 gRPC 代理

Spring Cloud Gateway 在 3.1.x 版本中增加了针对 gRPC 的网关代理功能支持,本片文章描述一下如何实现相关支持.本文主要基于 Spring Cloud Gateway 的 官方文档 进行一个实践练习。有兴趣的可以翻看官方文档。 由于 Grpc 是基于 HTTP2 协议进行传输的&#xff0c;因此 Srping …

zabbix监控进阶:如何分时段设置不同告警阈值(多阈值告警)

作者 乐维社区&#xff08;forum.lwops.cn&#xff09;乐乐 在生产环境中&#xff0c;企业的业务系统状态并不是一成不变的。在业务高峰时段&#xff0c;如节假日、促销活动或特定时间段&#xff0c;系统负载和用户访问量会大幅增加&#xff0c;此时可能需要设置更高的告警阈值…

vscode 使用正则将/deep/ 替换成 :deep()

在VSCODE编辑器的SEARCH中按上图书写即可&#xff0c;正则表达式如下&#xff1a;(\/deep\/)(.*?)(?\{) 替换操作如下&#xff1a;:deep($2) 如果有用,号隔开的用&#xff1a;(\/deep\/)(.*?)(?,)替换操作如下&#xff1a;:deep($2) 即可实现快速替换所有/deep/写法; 同理…

Cyuyanzhong的内存函数

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、memcpy函数的使用与模拟实现二、memmove函数的使用和模拟实现三、memset函数与memcmp函数的使用&#xff08;一&#xff09;、memset函数&#xff08;内存块…

一文速览Google的Gemma:从gemma1到gemma2

前言 如此文《七月论文审稿GPT第3.2版和第3.5版&#xff1a;通过paper-review数据集分别微调Mistral、gemma》所讲 Google作为曾经的AI老大&#xff0c;我司自然紧密关注&#xff0c;所以当Google总算开源了一个gemma 7b&#xff0c;作为有技术追求、技术信仰的我司&#xff0…

大模型ReAct:思考与工具协同完成复杂任务推理

ReAct: Synergizing Reasoning and Acting in Language Models Github&#xff1a;https://github.com/ysymyth/ReAct 一、动机 人类的认知通常具备一定的自我调节&#xff08;self-regulation&#xff09;和策略制定&#xff08;strategization&#xff09;的能力&#xff0…

福昕阅读器再打开PDF文件时,总是单页显示,如何设置打开后就自动显示单页连续的模式呢

希望默认进入连续模式 设置方法 参考链接 如何设置使福昕阅读器每次启动时不是阅读模式 每次启动后都要退出阅读模式 麻烦_百度知道 (baidu.com)https://zhidao.baidu.com/question/346796551.html#:~:text%E5%9C%A8%E3%80%90%E5%B7%A5%E5%85%B7%E3%80%91%E9%87%8C%E6%9C%89%E…

Springboot下使用Redis管道(pipeline)进行批量操作

之前有业务场景需要批量插入数据到Redis中&#xff0c;做的过程中也有一些感悟&#xff0c;因此记录下来&#xff0c;以防忘记。下面的内容会涉及到 分别使用for、管道处理批量操作&#xff0c;比较其所花费时间。 分别使用RedisCallback、SessionCallback进行Redis pipeline …

从零开始学Spring Boot系列-集成Spring Security实现用户认证与授权

在Web应用程序中&#xff0c;安全性是一个至关重要的方面。Spring Security是Spring框架的一个子项目&#xff0c;用于提供安全访问控制的功能。通过集成Spring Security&#xff0c;我们可以轻松实现用户认证、授权、加密、会话管理等安全功能。本篇文章将指导大家从零开始&am…

昇思25天学习打卡营第11天|基于MindSpore通过GPT实现情感分类

学AI还能赢奖品&#xff1f;每天30分钟&#xff0c;25天打通AI任督二脉 (qq.com) 基于MindSpore通过GPT实现情感分类 %%capture captured_output # 实验环境已经预装了mindspore2.2.14&#xff0c;如需更换mindspore版本&#xff0c;可更改下面mindspore的版本号 !pip uninsta…

Mysql常用SQL:日期转换成周_DAYOFWEEK(date)

有时候需要将查询出来的日期转换成周几&#xff0c;Mysql本身语法就是支持这种转换的&#xff0c;就是DAYOFWEEK()函数 语法格式&#xff1a;DAYOFWEEK(date) &#xff08;date&#xff1a;可以是指定的具体日期&#xff08; 如2024-06-29 &#xff09;&#xff0c;也可以是日期…

一个项目学习IOS开发---创建一个IOS开发项目

前提&#xff1a; 由于IOS开发只能在MacOS上开发&#xff0c;所以黑苹果或者购买一台MacBook Pro是每个IOS开发者必备的技能或者工具之一 Swift开发工具一般使用MacOS提供的Xcode开发工具 首先Mac Store下载Xcode工具 安装之后打开会提醒你安装IOS的SDK&#xff0c;安装好之…

媒体宣发套餐的概述及推广方法-华媒舍

在今天的数字化时代&#xff0c;对于产品和服务的宣传已经变得不可或缺。媒体宣发套餐作为一种高效的宣传方式&#xff0c;在帮助企业塑造品牌形象、扩大影响力方面扮演着重要角色。本文将揭秘媒体宣发套餐&#xff0c;为您呈现一条通往成功的路。 1. 媒体宣发套餐的概述 媒体…