介绍与应用
仓库: /
一、全文检索介绍 1.数据结构 结构化数据:
? 指具有“固定格式” 或“限定长度”的数据;
? 例如:数据库中的数据、元数据……
非结构化数据
? 指不定长度或无固定格式的数据;
? 例如:文本、图片、视频、图表……
2.数据的搜索 顺序扫描法
? 从第一个文件扫描到最后一个文件,把每一个文件内容从开头扫到结尾,直到扫完所有的文件 。
全文检索法
? 将非结构化数据中的一部分信息提取出来,重新组织,建立索引,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的 。
3.全文检索
例如:新华字典 。字典的拼音表和部首检字表就相当于字典的索引,我们可以通过查找索引从而找到具体的字解释 。如果没有创建索引,就要从字典的首页一页页地去查找 。
这种先建立索引 , 再对索引进行搜索的过程就叫全文检索(Full-text )。
全文检索的核心
创建索引:将从所有的结构化和非结构化数据提取信息,创建索引的过程 。
搜索索引:就是得到用户的查询请求,搜索创建的索引,然后返回结果的过程 。
4.倒排索引
倒排索引(英文:),也称为反向索引,是一种索引方法,实现“单词-文档矩阵”的一种具体存储形式,常被用于存储在全文搜索下某个单词与文档的存储位置的映射,通过倒排索引 , 可以根据单词快速获取包含这个单词的文档列表 。
倒排索引的结构主要由两个部分组成:“单词词典”和“倒排表” 。
索引方法例子
? 3个文档内容为:
? 1.php是过去最流行的语言 。
? 2.java是现在最流行的语言 。
? 3. 是未来流行的语言 。
倒排索引的创建
? 1.使用分词系统将文档切分成单词序列,每个文档就成了由由单词序列构成的数据流;
? 2.给不同的单词赋予唯一的单词id,记录下对应的单词;
? 3.同时记录单词出现的文档,形成倒排列表 。每个单词都指向了文档()链表 。
倒排索引的查询
? 假如说用户需要查询: “现在流行”
? 1.将用户输入进行分词,分为”现在”和”流行”;
? 2.取出包含字符串“现在”的文档链表;
? 3.取出包含字符串“流行”的文档链表;
? 4.通过合并链表,找出包含有”现在”或者”流行”的链表 。
倒排索引原理
当然倒排索引的结构也不是上面说的那么简单,索引系统还可以记录除此之外的更多信息 。词对应的倒排列表不仅记录了文档编号还记录了单词频率信息 。词频信息在搜索结果时,是重要的排序依据 。这里先了解下 , 后面的评分计算就要用到这个 。
索引和搜索流程图
二、入门
? 是一套用于全文检索和搜寻的开源程序库,由软件基金会支持和提供;
? 基于java的全文检索工具包, 并不是现成的搜索引擎产品,但可以用来制作搜索引擎产品;
? 官网:。
1.的总体结构
从的总体架构图可以看出:
? 1.库提供了创建索引和搜索索引的API 。
? 2.应用程序需要做的就是收集文档数据,创建索引;通过用户输入查询索引的得到返回结果 。
2.的几个基本概念
Index(索引):类似数据库的表的概念 , 但它完全没有约束,可以修改和添加里面的文档,文档里的内容可以任意定义 。
(文档):类似数据库内的行的概念,一个Index内会包含多个 。
Field(字段):一个会由一个或多个Field组成,分词就是对Field 分词 。
Term(词语)和Term (词典):中索引和搜索的最小单位,一个Field会由一个或多个Term组成 , Term是由Field经过(分词)产生 。Term 即Term词典,是根据条件查找Term的基本索引 。
3.创建索引过程
创建索引过程如下:
1.创建一个用来写索引文件,它有几个参数 , 就是索引文件所存放的位置,便是用来 对文档进行词法分析和语言处理的 。
2.创建一个代表我们要索引的文档 。将不同的Field加入到文档中 。不同类型的信息用不同的Field来表示
3.调用函数将索引写到索引文件夹中 。
4.搜索过程
搜索过程如下:
1.将磁盘上的索引信息读入到内存 , 就是索引文件存放的位置 。
2.创建准备进行搜索 。
3.创建用来对查询语句进行词法分析和语言处理 。
4.创建用来对查询语句进行语法分析 。
5.调用进行语法分析,形成查询语法树,放到Query中 。
6.调用对查询语法树Query进行搜索 , 得到结果 。
三、入门案例一 1.案例一代码
引入的jar包
public class LuceneTest {public static void main(String[] args) throws Exception {// 1. 准备中文分词器IKAnalyzer analyzer = new IKAnalyzer();// 2. 创建索引List productNames = new ArrayList<>();productNames.add("小天鹅TG100-1420WDXG");productNames.add("小天鹅TB80-easy60W 洗漂脱时间自由可调,京东微联智能APP手机控制");productNames.add("小天鹅TG90-1411DG 洗涤容量:9kg 脱水容量:9kg 显示屏:LED数码屏显示");productNames.add("小天鹅TP75-V602 流线蝶形波轮 , 超强喷淋漂洗");productNames.add("小天鹅TG100V20WDG 大件洗,无旋钮外观,智能WiFi");productNames.add("小天鹅TD80-1411DG 洗涤容量:8kg 脱水容量:8kg 显示屏:LED数码屏显示");productNames.add("海尔XQB90-BZ828 洗涤容量:9kg 脱水容量:9kg 显示屏:LED数码屏显示");productNames.add("海尔G100818HBG 极简智控面板,V6蒸汽烘干,深层洁净");productNames.add("海尔G100678HB14SU1 洗涤容量:10kg 脱水容量:10kg 显示屏:LED数码屏显");productNames.add("海尔XQB80-KM12688 智能自由洗 , 超净洗");productNames.add("海尔EG8014HB39GU1 手机智能,一键免熨烫,空气净化洗");productNames.add("海尔G100818BG 琥珀金机身,深层洁净,轻柔雪纺洗");productNames.add("海尔G100728BX12G 安全磁锁,健康下排水");productNames.add("西门子XQG80-WD12G4C01W 衣干即停,热风除菌,低噪音");productNames.add("西门子XQG80-WD12G4681W 智能烘干 , 变速节能 , 无刷电机");productNames.add("西门子XQG100-WM14U568LW 洗涤容量:10kg 脱水容量:10kg 显示屏:LED");productNames.add("西门子XQG80-WM10N1C80W 除菌、洗涤分离,防过敏程序");productNames.add("西门子XQG100-WM14U561HW 洗涤容量:10kg 脱水容量:10kg 显示屏:LED");productNames.add("西门子XQG80-WM12L2E88W 洗涤容量:8kg 脱水容量:8kg 显示屏:LED触摸");Directory index = createIndex(analyzer, productNames);// 3. 查询器String keyword = "西门子 LED";Query query = new QueryParser("name", analyzer).parse(keyword);// 4. 搜索IndexReader reader = DirectoryReader.open(index);IndexSearcher searcher = new IndexSearcher(reader);int numberPerPage = 1000;System.out.printf("当前一共有%d条数据%n"+"
", productNames.size());System.out.printf("查询关键字是:\"%s\"%n"+"
", keyword);ScoreDoc[] hits = searcher.search(query, numberPerPage).scoreDocs;// 5. 显示查询结果showSearchResults(searcher, hits, query, analyzer);// 6. 关闭查询reader.close();}private static void showSearchResults(IndexSearcher searcher, ScoreDoc[] hits, Query query, IKAnalyzer analyzer)throws Exception {System.out.println("找到 " + hits.length + " 个命中.
");System.out.println("序号\t匹配度得分\t结果
");SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("", "");Highlighter highlighter = new Highlighter(simpleHTMLFormatter, new QueryScorer(query));for (int i = 0; i < hits.length; ++i) {ScoreDoc scoreDoc= hits[i];int docId = scoreDoc.doc;Document d = searcher.doc(docId);List
");}}private static Directory createIndex(IKAnalyzer analyzer, List products) throws IOException {//存在内存中,新建一个词典Directory index = new RAMDirectory();IndexWriterConfig config = new IndexWriterConfig(analyzer);IndexWriter writer = new IndexWriter(index, config);for (String name : products) {addDoc(writer, name);}writer.close();return index;}/*** 添加文档内容* @param w* @param name* @throws IOException*/private static void addDoc(IndexWriter w, String name) throws IOException {//创建一个文档Document doc = new Document();doc.add(new TextField("name", name, Field.Store.YES));w.addDocument(doc);}}
2.代码解析 创建索引
private static Directory createIndex(IKAnalyzer analyzer, List products) throws IOException {//存在内存中,新建一个词典Directory index = new RAMDirectory();IndexWriterConfig config = new IndexWriterConfig(analyzer);IndexWriter writer = new IndexWriter(index, config);for (String name : products) {addDoc(writer, name);}writer.close();return index;}private static void addDoc(IndexWriter w, String name) throws IOException {//创建一个文档Document doc = new Document();doc.add(new TextField("name", name, Field.Store.YES));w.addDocument(doc);}
上面代码是将List中的内容保存在文档中,使用分词器分词,创建索引 , 索引保存在内存中 。对象用来写索引的 。
查询索引
// 3. 查询器String keyword = "西门子 智能";Query query = new QueryParser("name", analyzer).parse(keyword);// 4. 搜索IndexReader reader = DirectoryReader.open(index);IndexSearcher searcher = new IndexSearcher(reader);int numberPerPage = 1000;System.out.printf("当前一共有%d条数据%n", productNames.size());System.out.printf("查询关键字是:\"%s\"%n", keyword);ScoreDoc[] hits = searcher.search(query, numberPerPage).scoreDocs;// 5. 显示查询结果showSearchResults(searcher, hits, query, analyzer);// 6. 关闭查询reader.close();
上面代码是查询代码,首先对构建查询条件Query对象,读取索引,创建 查询对象,传入查询条件,得到查询结果,将结果解析出来,返回 。
分词器
创建索引和查询都要用到分词器,在中分词主要依靠类解析实现 。类是一个抽象类,分词的具体规则是由子类实现的,不同的语言规则 , 要有不同的分词器 , 默认的是不支持中文的分词 。
代码中用到了分词器 , 是第三方实现的分词器,继承自的类,针对中文文本进行处理的分词器 。
打分机制
从案例返回结果来看,有一列匹配度得分,得分越高的排在越前面,排在前面的查询结果也越准确 。
文章插图
打分公式:
?
? 库也实现了上面的打分算法,查询结果也会根据分数进行排序 。
高亮显示
SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("", "");Highlighter highlighter = new Highlighter(simpleHTMLFormatter, new QueryScorer(query));
将查询结果放到html页面,就会发现查询结果里关键字被标记为红色 。在 库的org....包中提供了关于高亮显示检索关键字的方法 , 可以对返回的结果中出现了的关键字进行标记 。
四、入门案例二 1.案例介绍
1.将14万条商品详细信息到mysql数据库;
2.使用库创建索引;
3.使用查询索引,并做分页操作,得到返回查询到的数据,并记录查询时长;
4.使用JDBC连接mysql数据库,采用like查询,对商品进行分页操作,返回查询到的数据,记录查询时长;
5.比较mysql的模糊查询与全文检索查询 。
2.案例二代码
引入的jar包,和mysql的驱动包,创建数据库表,插入数据.
/*** 商品bean类* @author yizl**/public class Product {/*** 商品id*/private int id;/*** 商品名称*/private String name;/*** 商品类型*/private String category;/*** 商品价格*/private float price;/*** 商品产地*/private String place;/*** 商品条形码*/private String code;...... }
public class TestLucene {private static ProductDao dao = new ProductDao();public static void main(String[] args) throws Exception {// 1. 准备中文分词器IKAnalyzer analyzer = new IKAnalyzer();// 2. 索引Directory index = createIndex(analyzer);// 3. 查询器Scanner s = new Scanner(System.in);while (true) {System.out.print("请输入查询关键字:");String keyword = s.nextLine();System.out.println("当前关键字是:" + keyword);long start = System.currentTimeMillis();// 查询名字字段Query query = new QueryParser("name", analyzer).parse(keyword);// 4. 搜索IndexReader reader = DirectoryReader.open(index);IndexSearcher searcher = new IndexSearcher(reader);ScoreDoc[] hits = pageSearch(query, searcher, 1, 10);// 5. 显示查询结果showSearchResults(searcher, hits, query, analyzer);// 6. 关闭查询reader.close();System.out.println("使用Lucene查询索引,耗时:" + (System.currentTimeMillis() - start) + "毫秒");System.out.println("-----------------------分割线-------------------------------");// 7.通过数据库进行模糊查询selectProductOfName(keyword);}}/*** 通过mysql商品名查询*/private static void selectProductOfName(String str) {long start = System.currentTimeMillis();ResultBean resultBean = dao.selectProductOfName(str, 1, 10);PageBean pageBean = resultBean.getPageBean();List products = resultBean.getData();System.out.println("查询出的总条数\t:" + pageBean.getTotal() + "条");System.out.println("当前第" + pageBean.getPageNow() + "页,每页显示" + pageBean.getPageSize() + "条数据");System.out.println("序号\t结果");for (int i = 0; i < products.size(); i++) {Product product = products.get(i);System.out.print((i + 1));System.out.print("\t" + product.getId());System.out.print("\t" + product.getName());System.out.print("\t" + product.getPrice());System.out.print("\t" + product.getPlace());System.out.print("\t" + product.getCode());System.out.println("
");}System.out.println("使用mysql查询,耗时:" + (System.currentTimeMillis() - start) + "毫秒");}/*** 显示找到的结果* * @param searcher* @param hits* @param query* @param analyzer* @throws Exception*/private static void showSearchResults(IndexSearcher searcher, ScoreDoc[] hits, Query query, IKAnalyzer analyzer)throws Exception {System.out.println("序号\t匹配度得分\t结果");for (int i = 0; i < hits.length; ++i) {ScoreDoc scoreDoc = hits[i];int docId = scoreDoc.doc;Document d = searcher.doc(docId);List fields = d.getFields();System.out.print((i + 1));System.out.print("\t" + scoreDoc.score);for (IndexableField f : fields) {System.out.print("\t" + d.get(f.name()));}System.out.println("
");}}/*** 分页查询* * @param query* @param searcher* @param pageNow*当前第几页* @param pageSize*每页显示条数* @return* @throws IOException*/private static ScoreDoc[] pageSearch(Query query, IndexSearcher searcher, int pageNow, int pageSize)throws IOException {TopDocs topDocs = searcher.search(query, pageNow * pageSize);System.out.println("查询到的总条数\t" + topDocs.totalHits);System.out.println("当前第" + pageNow + "页,每页显示" + pageSize + "条数据");ScoreDoc[] alllScores = topDocs.scoreDocs;List hitScores = new ArrayList<>();int start = (pageNow - 1) * pageSize;int end = pageSize * pageNow;for (int i = start; i < end; i++)hitScores.add(alllScores[i]);ScoreDoc[] hits = hitScores.toArray(new ScoreDoc[] {});return hits;}/*** 创建Index,将数据存入内存中* * @param analyzer* @return* @throws IOException*/private static Directory createIndex(IKAnalyzer analyzer) throws IOException {long start = System.currentTimeMillis();Directory index = new RAMDirectory();IndexWriterConfig config = new IndexWriterConfig(analyzer);IndexWriter writer = new IndexWriter(index, config);List products = dao.selectAllProduct();int total = products.size();int count = 0;int per = 0;int oldPer = 0;for (Product p : products) {addDoc(writer, p);count++;per = count * 100 / total;if (per != oldPer) {oldPer = per;System.out.printf("索引中,总共要添加 %d 条记录,当前添加进度是: %d%% %n", total, per);}}System.out.println("索引创建耗时:" + (System.currentTimeMillis() - start) + "毫秒");writer.close();return index;}/*** 往lucene中添加字段* * @param w* @param p* @throws IOException*/private static void addDoc(IndexWriter w, Product p) throws IOException {Document doc = new Document();doc.add(new TextField("id", String.valueOf(p.getId()), Field.Store.YES));doc.add(new TextField("name", p.getName(), Field.Store.YES));doc.add(new TextField("category", p.getCategory(), Field.Store.YES));doc.add(new TextField("price", String.valueOf(p.getPrice()), Field.Store.YES));doc.add(new TextField("place", p.getPlace(), Field.Store.YES));doc.add(new TextField("code", p.getCode(), Field.Store.YES));w.addDocument(doc);}}
public class ProductDao {private static String url = "jdbc:mysql://localhost:3306/lucene?useUnicode=true&characterEncoding=utf8";private static String user = "root";private static String password = "root";public static Connection getConnection() throws ClassNotFoundException, SQLException {Connection conn = null;// 通过工具类获取连接对象Class.forName("com.mysql.jdbc.Driver");conn = DriverManager.getConnection(url, user, password);return conn;}/*** 批量增加商品* @param pList*/public void insertProduct(List pList) {String insertProductTop="INSERT INTO `product` (`id`, `name`, "+ "`category`, `price`, `place`, `code`) VALUES ";Connection conn = null;Statement stmt = null;try {conn = getConnection();// 3.创建Statement对象stmt = conn.createStatement();int count=0;// 4.sql语句StringBuffer sb = new StringBuffer();for (int i = 0,len=pList.size(); i < len; i++) {Product product = pList.get(i);sb.append("(" + product.getId() + ",'" + product.getName() + "','" + product.getCategory()+ "'," + product.getPrice()+ ",'" + product.getPlace() + "','" + product.getCode() + "')");if (i==len-1) {sb.append(";");break;}else {sb.append(",");}//数据量太大会导致一次执行不了,一次最多执行20000条if(i%20000==0&&i!=0) {sb.deleteCharAt(sb.length()-1);sb.append(";");String sql = insertProductTop+sb;count += stmt.executeUpdate(sql);//将sb清空sb.delete(0, sb.length());}}String sql = insertProductTop+sb;// 5.执行sqlcount += stmt.executeUpdate(sql);System.out.println("影响了" + count + "行");} catch (Exception e) {e.printStackTrace();throw new RuntimeException(e);} finally {close(conn, stmt);}}/*** 关闭资源* @param conn* @param stmt*/private void close(Connection conn, Statement stmt) {// 关闭资源if (stmt != null) {try {stmt.close();} catch (SQLException e) {e.printStackTrace();throw new RuntimeException(e);}}if (conn != null) {try {conn.close();} catch (SQLException e) {e.printStackTrace();throw new RuntimeException(e);}}}// public void deleteAllProduct() {Connection conn = null;Statement stmt = null;try {conn = getConnection();// 3.创建Statement对象stmt = conn.createStatement();// 4.sql语句String sql = "delete from product";// 5.执行sqlint count = stmt.executeUpdate(sql);System.out.println("影响了" + count + "行");} catch (Exception e) {e.printStackTrace();throw new RuntimeException(e);} finally {// 关闭资源close(conn, stmt);}}/*** 查询所有商品*/public List selectAllProduct() {List pList=new ArrayList<>();Connection conn = null;Statement stmt = null;try {conn = getConnection();// 3.创建Statement对象stmt = conn.createStatement();// 4.sql语句String sql = "select * from product";// 5.执行sqlResultSet rs = stmt.executeQuery(sql);while (rs.next()) {Product product=new Product();product.setId(rs.getInt("id"));product.setName(rs.getString("name"));product.setCategory(rs.getString("category"));product.setPlace(rs.getString("place"));product.setPrice(rs.getFloat("price"));product.setCode(rs.getString("code"));pList.add(product);}} catch (Exception e) {e.printStackTrace();throw new RuntimeException(e);} finally {// 关闭资源close(conn, stmt);}return pList;}/*** 通过商品名模糊匹配商品* @param strName* @param pageNow* @param pageSize* @return*/public ResultBean selectProductOfName(String strName, int pageNow, int pageSize) {ResultBean resultBean=new ResultBean();PageBean pageBean =new PageBean();pageBean.setPageNow(pageNow);pageBean.setPageSize(pageSize);List pList=new ArrayList<>();Connection conn = null;PreparedStatement pstmt = null;try {conn = getConnection();// sql语句String sql = "SELECT id,name,category,place,price,code FROM product"+ " where name like ? limit "+(pageNow-1)*pageSize+","+pageSize; // 3.创建PreparedStatement对象,sql预编译pstmt = conn.prepareStatement(sql);// 4.设定参数pstmt.setString(1, "%" + strName + "%" );// 5.执行sql,获取查询的结果集ResultSet rs = pstmt.executeQuery();while (rs.next()) {Product product=new Product();product.setId(rs.getInt("id"));product.setName(rs.getString("name"));product.setCategory(rs.getString("category"));product.setPlace(rs.getString("place"));product.setPrice(rs.getFloat("price"));product.setCode(rs.getString("code"));pList.add(product);}String selectCount = "SELECT count(1) c FROM product"+ " where name like ? ";pstmt = conn.prepareStatement(selectCount);pstmt.setString(1, "%" + strName + "%" ); ResultSet rs1 = pstmt.executeQuery();int count=0;while (rs1.next()) {count = rs1.getInt("c");}pageBean.setTotal(count);resultBean.setPageBean(pageBean);resultBean.setData(pList);} catch (Exception e) {e.printStackTrace();throw new RuntimeException(e);} finally {// 关闭资源if (pstmt != null) {try {pstmt.close();} catch (SQLException e) {e.printStackTrace();throw new RuntimeException(e);}}if (conn != null) {try {conn.close();} catch (SQLException e) {e.printStackTrace();throw new RuntimeException(e);}}}return resultBean;}}
/*** 返回结果bean* @author yizl** @param */public class ResultBean {/*** 分页信息*/private PageBean pageBean;/*** 状态码*/private Integer code;/*** 提示信息*/private String msg;/*** 返回数据*/private T data;
/*** 分页bean* @author yizl**/public class PageBean {/*** 当前页数*/private Integer pageNow;/*** 每页条数*/private Integer pageSize;/*** 总数*/private Integer total;
4.的分页查询
private static ScoreDoc[] pageSearch(Query query, IndexSearcher searcher, int pageNow, int pageSize)throws IOException {TopDocs topDocs = searcher.search(query, pageNow * pageSize);System.out.println("查询到的总条数\t" + topDocs.totalHits);System.out.println("当前第" + pageNow + "页,每页显示" + pageSize + "条数据");ScoreDoc[] alllScores = topDocs.scoreDocs;List hitScores = new ArrayList<>();int start = (pageNow - 1) * pageSize;int end = pageSize * pageNow;for (int i = start; i < end; i++)hitScores.add(alllScores[i]);ScoreDoc[] hits = hitScores.toArray(new ScoreDoc[] {});return hits;}
先把所有的命中数查询出来 , 在进行分页,有点是查询快,缺点是内存消耗大 。
5.结果比较分析
1.14万条数据,从创建索引耗时:11678毫秒,创建索引还是比较耗时的,但是索引只用创建一次,后面都查询都可以使用;
2.从查询时间来看,使用查询,基本都在10ms左右,mysql查询耗时在150ms以上,查询速度方面有很大的提升 , 特别是数据量大的时候更加明显;
3.从查询精准度来说,输入单个的词语可能都能查询到结果 , 输入组合词语,mysql可以匹配不了,依然可以查询出来,将匹配度高的结果排在前面,更精准 。
6.索引与mysql数据库对比 全文检索mysql数据库
索引
将数据源中的数据–建立反向索引,查询快
对于like查询来说,传统数据库的索引不起作用,还是要全表扫描,查询慢
匹配效果
词元(term)匹配,通过语言分析接口进行关键字拆分,匹配度高
模糊匹配,可能不能匹配相关的词组
匹配度
有匹配度算法,匹配度高的得分高排前面
无匹配程度算法,随机排列
关键字标记
提供高亮显示的Api,可以对查询结果的关键字高亮标记
没有直接使用的api,需要自己封装
五、总结
【Lucene介绍与应用】首先我们了解全文检索方法,全文检索搜索非结构化数据速度快等优点,倒排索引是现在最常用的全文检索方法,索引的核心就是怎么创建索引和查询索引 。至于怎么实现创建和查询,软件基金会很贴心的为我们Java程序员提供了开源库,它为我们提供了创建和查询索引的api,这就是我们学习的目的 。
- 国秀和志宏离了几次婚 志宏与国秀
- 剑与远征沉寂荒墟怎么打 剑与远征沉寂荒墟怎么打boss
- 光与夜之恋选择 光与夜之恋安安怎么选
- 鬼灭之刃介绍 鬼灭之刃介绍简短
- 安脏汤的功效与作用及禁忌 安脏汤的功效与作用
- 安枕无忧散的功效与作用及禁忌 安枕无忧散的功效与作用
- 安贞汤的功效与作用 安贞汤的功效与作用及禁忌
- 田野草的功效与作用图片 田野草的功效与作用
- 钱串草有什么作用 串钱草的功效与作用
- 仙桃草的功效与作用 仙桃草的功效与作用图片