1. 总结
1、SolrCore的配置
a)schma.xml文件
b)配置中文分析器
2、配置业务域和批量索引导入
a)配置业务域
b)批量索引导入
c)Solrj复杂查询(用Query页面复杂查询、用程序实现)
3、京东案例(简单的站内搜索实现)
2.SolrCore的配置(重点)
SolrCore的运行由两个重要的配置文件做指导,一个是solrconfig.xml,一个是schema.xml。
1)solrconfig.xml配置:
SolrCore运行的核心要素(依赖包、数据目录)和基本功能:(索引CRUD的web请求处理器)等,这是SolrCore的核心配置文件。之前已经介绍过了。
2)schema.xml配置:
·域以及域类型的配置:
SolrCore中的所有Field域都采用先配置才能使用的方式。域的配置必须在schema.xml中。
SolrCore本身自带了一些域的定义以及这些域的类型定义,但就像MySQL数据库刚创建完就会有一个默认的mysql数据库一样,我们根本不会理睬里面有什么,也根本不用,都是我们自己根据实际业务需求重新定义自己的数据库。SolrCore中的Field也是一样,它自带的基本什么用都没有,这个我们昨天也看到了。我们真正使用的都是根据我们业务需求重新创建Field域。
2.1. schema.xml
schema.xml文件在SolrCore的conf目录下,此配置文件中不仅定义了域还定义了域的类型。在solr中的域必须先定义后使用。
2.1.1. field
<field name="id" type="string"indexed="true" stored="true"required="true" multiValued="false"/>
l name(必须):域的名称
l indexed(必须):是否索引,在Lucene学习中我们知道是否索引是由域的类型决定的,但是solr在此做了域的增强,可以直接在域的配置中自行决定是否索引了。
l stored(必须):是否存储
(注意:似乎缺少一个【是否分词】的配置属性,通过Lucene学习我们知道是否分词是由域的类型决定的,Solr在此处仍然保持了这个特性,由域的类型决定是否分词。
l type(必须):域类型的别名,Solr可以给域类型配置所需的分析器,然后给这个配置起个别名,然后这个别名就可以用来定义域的配置。这样设计的好处是同一种域类型根据需求可以配置出多种不同的分词能力,可以获得更好的扩展性。
l required(可选):是否为必须项,true表示文档中必须存在该域,反之为false,不配置时默认为false(schema.xml中只配置了id域为必须项)
l multiValued(可选):是否允许域中保存多个值,true表示允许,反之为false,不配置时默认为false。
多值的情况:比如存储一个商品的多个图片地址(大图、小图、缩略图)。
2.1.2. fieldType(域类型配置)
Solr可以针对它的域类型做不同的配置:这里面最重要的是配置分析器。
<fieldType name="text_general"class="solr.TextField" positionIncrementGap="100">
<analyzer type="index">
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt"/>
<!-- in this example, we will only use
synonyms at query time
<filter
class="solr.SynonymFilterFactory"
synonyms="index_synonyms.txt" ignoreCase="true"
expand="false"/>
-->
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt"/>
<filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
l name:域类型的别名
l class:指定solr的域类型。
l analyzer:指定分词器,包括分词器和各种过滤器。
还可以直接指定一个分析器的class类,这样就使用这个分析器中默认的分词器和过滤器,而不需要再配置分词器和过滤器了。这样的配置更加简化。
l type:两个值:index和query。index
是创建索引,query是查询索引。
l tokenizer:指定分词器
l filter:指定过滤器
2.1.3. dynamicField(动态域)
<dynamicField name="*_s" type="string" indexed="true" stored="true"/>
name:动态域的名称,是一个表达式,*匹配任意字符,只要域的名称和表达式的规则能够匹配就可以使用。
其他属性和正常field域配置一样,没有区别。
例如:添加索引id=zjl001,zhoujielun_s=zhoujielun001,其中zhoujielun_s在schema.xml中没有配置,但是这个符合动态域【*_s】,所以仍然可以添加:
@Test public void testCreateIndex() throws Exception { // URL String
baseUrl = "http://localhost:8081/solr/collection1"; // 创建HttpSolrServer HttpSolrServer
solrServer = new HttpSolrServer(baseUrl); // 创建document对象 SolrInputDocument
document = new SolrInputDocument(); document.addField("id", "zjl001"); document.addField("zhoujielun_s", "zhoujielun001"); solrServer.add(document); solrServer.commit(); } |
2.1.4. uniqueKey
<uniqueKey>id</uniqueKey>
强制声明该域在文档对象中必须是唯一的,相当于主键。
2.1.5. copyField(复制域)
<copyField source="cat"dest="text"/>
Solr支持将多个域的内容拷贝到一个新的域中,好处是对拷贝后的新域的内容进行查询就是对多个域的内容进行统一的检索。
l source:源域
l dest:目标域,将源域拷贝到目标域时,solr会对拷贝到一起的内容做优化处理,这样搜索时,指定目标域为默认搜索域,可以提高查询效率。
l 无论是源还是目标都得是已经存在的域,并且目标域的multiValued推荐为true。
定义目标域:
<field name="text"type="text_general" indexed="true" stored="false"multiValued="true"/>
2.2. 配置中文分析器
中文分析器需要在Solr服务中配置,使用IKAnalyzer中文分析器。
第一步:将IK分析器的jar包配置到Solr服务中。
把IKAnalyzer2012FF_u1.jar添加到solr/WEB-INF/lib目录下。
第二步:将IK需要的配置文件和词库加到Solr服务中
在solr/WEB-INF/下创建classes文件夹,用于存放IK分析器词库配置文件、自定义扩展词库、停用词词库。然后把这些文件复制solr/WEB-INF/classes目录下。
第三步:将日志配置文件添加到Solr服务中
如果想在cmd中输出日志需要在classes下添加log4j.properties
第四步:在schema.xml中配置IK分析器
在schema.xml中添加一个自定义的fieldType,使用中文分析器。
<!--
fileType with IKAnalyzer -->
<fieldType name="text_ik"class="solr.TextField">
<analyzer class="org.wltea.analyzer.lucene.IKAnalyzer"/>
</fieldType>
第五步:在schema.xml中配置支持IK的域
在schema.xml中添加field,指定field的type属性为text_ik
<!--
IKAnalyzer Field -->
<field name="content_ik"type="text_ik" indexed="true"stored="true" />
第六步:重启tomcat
效果:
3.以业务需求进行域的配置和批量索引(重点)
3.1. 业务需求
如果有一个创建站内搜索的需求,就需要将数据库中的数据导入到SolrCore中创建索引库,然后利用solr服务实现网站中站内商品搜索。
1. 需要确定数据库中检索出来的字段和数据
2. 根据数据库中查询出的字段在SolrCore的schema.xml中配置业务需要的Field域。
3. 需要实现一个能够批量导入数据到SolrCore索引库的功能。
4. 测试导入的索引
3.2. 数据库的字段和数据
3.2.1. 导入数据SQL脚本
在数据库中运行solr.sql脚本
导入数据:总共3803条数据
3.2.2. 查询SQL
SELECT
pid,name,catalog_name,price,description,picture,release_time FROM products
3.3. 定义业务Field域
先确定定义的商品document的Field域有哪些?
可以根据我们要查询的有哪些商品表的字段来确定:
products商品表:
在SolrCore的schema.xml中配置业务域,就根据我们检索的字段来创建:
<!--
products Field -->
<field name="product_name"type="text_ik" indexed="true"stored="true" />
<field name="product_catalog_name"type="string" indexed="true"stored="true" />
<field name="product_price"type="float" indexed="true"stored="true" />
<field name="product_description"type="text_ik" indexed="true"stored="false" />
<field name="product_picture"type="string" indexed="false"stored="true" />
<field name="product_release_time"type="date" indexed="true"stored="true" />
<!--
products copyField -->
<field name="product_keywords"type="text_ik" indexed="true"stored="false" multiValued="true"/>
<copyField source="product_name"dest="product_keywords"/>
<copyField source="product_description"dest="product_keywords"/>
注意:这里没有创建id的field,因为在schema.xml中默认自带id的field,而且必须包含这个id域,所以这里不用建。
3.4. 批量导入索引
在昨天也介绍solr的可视化管理工具中有一个Dataimport功能,就是用于批量导入数据创建索引的:
在上一节我们已经事先配置了业务域,下面需要利用批量导入插件将mysql中的products表中的数据批量创建索引:
3.4.1. 第一步:导入插件依赖jar
先在一个SolrCore实例(collection1)下创建一个lib文件夹,然后把dataimport插件依赖的jar包添加到lib中, 同时还需要mysql的数据库驱动。
3.4.2. 第二步:配置solrconfig.xml
配置solrconfig.xml文件,添加一个requestHandler。
<!-- A
request handler that dataimport -->
<requestHandler name="/dataimport"class="org.apache.solr.handler.dataimport.DataImportHandler">
<lst name="defaults">
<str name="config">data-config.xml</str>
</lst>
</requestHandler>
创建一个data-config.xml,与solrconfig.xml保存到同目录下
<?xml version="1.0"encoding="UTF-8" ?>
<dataConfig>
<dataSource type="JdbcDataSource"
driver="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/lucene"
user="root"
password="123"/>
<document>
<entity name="product"transformer="DateFormatTransformer"
query="SELECT
pid,name,catalog_name,price,description,picture,release_time FROM
products">
<field column="pid"name="id"/>
<field column="name"name="product_name"/>
<field column="catalog_name"name="product_catalog_name"/>
<field column="price"name="product_price"/>
<field column="description"name="product_description"/>
<field column="picture"name="product_picture"/>
<field column="release_time"name="product_release_time" dateTimeFormat="yyyy-MM-dd HH:mm:ss"/>
</entity>
</document>
</dataConfig>
上面的配置也可以从帮助文档中获得:
会跳转到solr的wiki:需要连网访问
从中找到一个合适的配置即可:
关于时间的配置:
搜索【DateFormat】即可搜索到关于日期时间的相关配置:
【DateFormatTransformer】属于【Transformer】的一个值:
3.4.3. 第三步:重启tomcat
重启tomcat,然后刷新页面,点击Dataimport就可以看到这个导入界面了:
点击“execute”按钮导入数据。
界面重要选项说明:
·Command:
full-import表示全部导入
delta-import表示差分导入(把缺少的部分导入进来)
一般我们为了导入的数据完整,都采用full-import。
·Clean:(默认选中)表示导入数据前会自动清空之前的索引文件
·Commit:(默认选中)表示创建的索引会自动提交
·Auto-Refresh
Status:(默认不选中)它是控制到导入数据过程中界面是否自动刷新,默认不自动,要想自动选中即可。
导入成功界面:
3.5. 测试导入的索引
3.5.1. solr的查询语法
1. q:查询关键字,必须的。
请求的q是字符串,如果查询所有使用*:*
2. fq: (filter query)过滤查询
作用:在q查询符合结果中同时是fq查询符合的
请求fq是一个数组(多个值)
过滤查询价格从1到20的记录。
也可以使用“*”表示无限,例如:
20以上:product_price:[20 TO *]
20以下:product_price:[* TO 20]
也可以在“q”查询条件中使用product_price:[1 TO 20],
如下效果和上面一样:
3. sort: 排序,desc代表降序,asc代表升序
按照价格升序排
4. start: 分页显示使用,开始记录下标,从0开始
rows: 指定返回结果最多有多少条记录,配合start来实现分页。
5. fl: (Field List)指定返回那些字段内容,用逗号或空格分隔多个。
显示商品id、商品名称、商品分类名称
6. df: 指定默认搜索Field
7. wt: (writer type)指定输出格式,可以有 xml, json, php, python等
8. hl: 是否高亮 ,设置高亮Field,设置格式前缀和后缀。
3.5.2. solrj的复杂查询
页面的查询条件,复杂查询条件和页面的查询条件一致
上面的查询条件也可以用代码实现:
/** * solrj复杂查询 * * @throws Exception */ @Test public void complexSearchIndexTest() throws Exception { // 查询条件设置 SolrQuery
solrQuery = setQueryConditions(); // 执行查询 QueryResponse
response = solrServer.query(solrQuery); // 处理查询查询结果 dealSearchResult(response); } // 查询条件设置 private SolrQuery setQueryConditions() throws Exception { SolrQuery
query = new SolrQuery(); // 设置查询条件 query.setQuery("台灯"); // 设置过滤条件 query.setFilterQueries("product_catalog_name:雅致灯饰", "product_price:[30 TO 40]"); // 或者分开设置 /*query.setFilterQueries("product_catalog_name:雅致灯饰"); query.setFilterQueries("product_price:[30
TO 40]");*/ // 设置排序条件 query.setSort("product_price", ORDER.desc); // 设置分页信息(开始行号和每页数据量) query.setStart(0); query.setRows(10); // 设置需要显示的域名(可选,不设置时全部显示) //query.setFields("id",
"product_name", "product_price"); // 设置默认搜索域 query.set("df", "product_name"); // 设置高亮显示(是否开启高亮显示,高亮显示的域名,高亮的前缀和后缀) query.setHighlight(true); query.addHighlightField("product_name"); query.setHighlightSimplePre("<font
color="red">"); query.setHighlightSimplePost("</font>"); return query; } // 处理查询结果 private void dealSearchResult(QueryResponse response) throws Exception { // 取得商品信息结果集 SolrDocumentList
results = response.getResults(); // 取得高亮信息结果集 Map<String,
Map<String, List<String>>> hlResults = response.getHighlighting(); // 打印结果件数 System.out.println("查询结果总件数:" + results.getNumFound()); // 遍历结果集打印结果 for (SolrDocument doc : results) { System.out.println("=================================="); // 处理高亮显示的内容 String
hlValue = dealHighlighting(hlResults, doc, "product_name"); // 输出结果 System.out.println("id:" + doc.get("id")); System.out.println("product_name:" + hlValue); System.out.println("product_price:" + doc.get("product_price")); System.out.println("product_catalog_name:" + doc.get("product_catalog_name")); System.out.println("product_picture:" + doc.get("product_picture")); } } // 处理高亮显示内容 private String
dealHighlighting(Map<String, Map<String, List<String>>> hlResults, SolrDocument
doc, String hlField) throws Exception { // 先取得原版的域值 String
orgFieldValue = (String)doc.get(hlField); // 判断高亮结果集中是否包含当前文档的高亮信息 if (hlResults != null) { List<String> list = hlResults.get(doc.get("id")).get(hlField); if (list != null && list.size() > 0) { return list.get(0); } } return orgFieldValue; } |
4.案例(重点)
4.1. 需求
使用Solr实现电商网站中商品信息搜索功能,可以根据关键字搜索商品信息,根据商品分类、价格过滤搜索结果,也可以根据价格进行排序,实现分页。
界面如下:
4.1.1. 架构分析
架构分为:
1、 solr服务器
2、 自己的web服务器(需要开发)
3、 数据库mysql
自己开发的应用
1、 Controller
获取搜索条件,调用查询站内搜索service进行查询,并响应搜索结果到前台页面。
2、 Service
Service调用dao, 用前台传入的查询条件创建SolrQuery对象,然后传给DAO,进行搜索。
Service调用dao进行商品数据的维护时,要同步更新索引库(本案例不实现)
3、 Dao
根据service传入的SolrQuery对象,对solr的索引库进行搜索,并返回查询结果。
对商品数据进行维护和查询(本案例不实现)
4.2. 环境准备
l Solr:4.10.3
l Jdk环境:1.7
l IDE环境:eclipse Mars2
l 服务器:Tomcat 7
4.3. 工程搭建(UTF-8)
创建一个web工程导入jar包
1、spring的相关jar包
2、solrJ的jar包和依赖jar包
3、Solr服务的日志依赖包,solrexamplelibext下的jar包
创建后完整的工程:
1. 导入jar包后,先创建目录:dao、pojo、service、controller、config、jsp
2. 导入配置文件:
1) 表现层SpringMVC.xml:@controller注解扫描,注解驱动,视图解析器
2) 业务层ApplicationContext.xml:@Service、@Repository注解扫描、solrj连接Solr服务的bean
(注意:这里面没有集成mybatis,所以我们用注解@Repository来注入,这样DAO需要有实现类我们自己编写。)
3) web.xml:SpringMVC的前端控制器和Spring监听
4)
log4j.properties
3. 导入jsp页面:【资料jd案例product_list.jsp】
4. 导入静态资源:【资料jd案例】下的【images】【resource】拷贝到工程【WebContent】下。
5. 导入pojo:【资料jd案例pojo】下的【ProductModel.java】【ResultModel.java】
6. ApplicationContext.xml中配置SolrServer:
<!-- 配置solr server bean --> <bean id="solrServer"class="org.apache.solr.client.solrj.impl.HttpSolrServer"> <!-- 配置Solr Server实例时需要的构造函数的参数 --> <constructor-arg value="http://127.0.0.1:8081/solr/collection1"/> </bean> |
4.4. Dao
功能:接收service层传递过来的参数,根据参数查询索引库,返回查询结果。
参数:SolrQuery对象
返回值:一个商品列表List<ProductModel>,还需要返回查询结果的总数量。
返回:ResultModel
方法定义:ResultModel
searchProductList(SolrQuery query) throws Exception;
商品对象模型:
public class ProductModel {
// 商品编号
private String pid;
// 商品名称
private String name;
// 商品分类名称
private String catalog_name;
// 价格
private float price;
// 商品描述
private String description;
// 图片名称
private String picture;
// setter/getter......
}
返回值对象模型
public class ResultModel {
// 商品列表
private List<ProductModel> productList;
// 商品总数
private Long recordCount;
// 总页数
private int pageCount;
// 当前页
private int curPage;
// setter/getter......
}
package cn.itcast.dao; import java.util.ArrayList; import java.util.List; import java.util.Map; import
org.apache.solr.client.solrj.SolrQuery; import
org.apache.solr.client.solrj.SolrServer; import
org.apache.solr.client.solrj.response.QueryResponse; import
org.apache.solr.common.SolrDocument; import
org.apache.solr.common.SolrDocumentList; import
org.springframework.beans.factory.annotation.Autowired; import
org.springframework.stereotype.Repository; import cn.itcast.pojo.ProductModel; import cn.itcast.pojo.ResultModel; @Repository public class ProductDaoImpl implements ProductDao { @Autowired private SolrServer solrServer; @Override public ResultModel
searchProductList(SolrQuery query) throws Exception { // 执行查询,并返回结果 QueryResponse
queryResponse = solrServer.query(query); // 从结果对象中取得结果集 SolrDocumentList
results = queryResponse.getResults(); // 取得高亮信息结果集 Map<String,
Map<String, List<String>>> hlResults = queryResponse.getHighlighting(); // 创建返回结果对象 ResultModel
resultModel = new ResultModel(); // 创建结果集list List<ProductModel>
productList = new ArrayList<ProductModel>(); // 查询件数 resultModel.setRecordCount(results.getNumFound()); // 遍历结果集打印详细内容 for (SolrDocument doc : results) { // 创建一个ProductModel查询结果 ProductModel
productModel = new ProductModel(); // 取得高亮显示的信息,然后根据高亮显示的信息进行结果信息打印 String
productName = (String)doc.get("product_name"); if (hlResults != null) { List<String>
list = highlighting.get(doc.get("id")).get("product_name"); if (list != null && list.size() > 0) { productName = list.get(0); } } // 商品id productModel.setPid(String.valueOf(doc.get("id"))); // 商品名称 productModel.setName(productName); // 商品价格 String
price = String.valueOf(doc.get("product_price")); if (price != null && !"".equals(price)) { productModel.setPrice(Float.parseFloat(price)); } // 商品图片 productModel.setPicture(String.valueOf(doc.get("product_picture"))); // 添加到商品列表中 productList.add(productModel); } if (productList.size() > 0) { resultModel.setProductList(productList); } return resultModel; } } |
4.5. Service
功能:接收action传递过来的参数,根据参数拼装一个查询条件,调用dao层方法,查询商品列表。接收返回的商品列表和商品的总数量,根据每页显示的商品数量计算总页数。
参数:
1、查询条件:字符串
2、商品分类的过滤条件:商品的分类名称,字符串
3、商品价格区间:传递一个字符串,满足格式:“0-100、101-200、201-*”
4、排序条件:页面传递过来一个升序或者降序就可以,默认是价格排序。0:升序1:降序
5、分页信息:每页显示的记录条数创建一个常量60条。传递一个当前页码就可以了。
返回值:ResultModel
方法定义:ResultModel
queryProduct(String queryString, String catalog_name, String price, String
sort, Integer page) throws Exception;
package cn.itcast.service; import
org.apache.solr.client.solrj.SolrQuery; import
org.apache.solr.client.solrj.SolrQuery.ORDER; import
org.springframework.beans.factory.annotation.Autowired; import
org.springframework.stereotype.Service; import cn.itcast.dao.ProductDao; import cn.itcast.pojo.ResultModel; @Service public class ProductServiceImpl implements ProductService { // 每页显示60条数据 private static final Integer PAGE_SIZE = 60; @Autowired private ProductDao prodDao; @Override public ResultModel search(String queryString, String catalog_name, String
price, Integer page, String sort) throws Exception { // 封装查询条件 SolrQuery
solrQuery = new SolrQuery(); // 设置默认查询域 solrQuery.set("df", "product_keywords"); // 设置查询条件 if (queryString != null && !"".equals(queryString)) { solrQuery.setQuery(queryString); }
else { solrQuery.setQuery("*:*"); } // 根据分类过滤 if (catalog_name != null && !"".equals(catalog_name)) { solrQuery.addFilterQuery("product_catalog_name:" + catalog_name); }
// else { // // 注意这里要说一下为什么没有else,因为过滤条件不是必须的所以可以没有. // } // 价格过滤 if (price != null && !"".equals(price)) { String[]
split = price.split("-"); if (split != null && split.length > 0) { solrQuery.addFilterQuery("product_price:[" + split[0] + " TO " + split[1] + "]"); } } // 排序 if ("1".equals(sort)) { solrQuery.setSort("product_price", ORDER.asc); }
else { solrQuery.setSort("product_price", ORDER.desc); } // 分页 if (page == null || page <= 0) { page = 1; } // 从第几条开始 solrQuery.setStart((page - 1) * PAGE_SIZE); // 每页显示多少条 solrQuery.setRows(PAGE_SIZE); // 设置高亮 solrQuery.setHighlight(true); // 设置高亮显示的域 solrQuery.addHighlightField("product_name"); // 设置高亮前缀 solrQuery.setHighlightSimplePre("<span
style="color:red">"); // 设置高亮后缀 solrQuery.setHighlightSimplePost("</span>"); // 根据查询条件取得结果 ResultModel
resultModel = prodDao.search(solrQuery); // 设置当前页 resultModel.setCurPage(page); // 计算总页数 Long
pageCount = resultModel.getRecordCount() / PAGE_SIZE; if (resultModel.getRecordCount() % PAGE_SIZE > 0) { pageCount++; } resultModel.setPageCount(pageCount); return resultModel; } } |
4.1. Controller
功能:接收页面传递过来的参数调用service查询商品列表。将查询结果返回给jsp页面,还需要查询参数的回显。
参数:
1、查询条件:字符串
2、商品分类的过滤条件:商品的分类名称,字符串
3、商品价格区间:传递一个字符串,满足格式:“0-100、101-200、201-*”
4、排序条件:页面传递过来一个升序或者降序就可以,默认是价格排序。0:升序1:降序
5、分页信息:每页显示的记录条数创建一个常量60条。传递一个当前页码就可以了。
6、Model:相当于request。
返回结果:String类型,就是一个jsp的名称。
String queryProduct(String queryString, String
caltalog_name, String price, String sort, Integer page, Model model) throws
Exception;
package cn.itcast.controller; import
org.springframework.beans.factory.annotation.Autowired; import
org.springframework.stereotype.Controller; import org.springframework.ui.Model; import
org.springframework.web.bind.annotation.RequestMapping; import cn.itcast.pojo.ResultModel; import cn.itcast.service.ProductService; @Controller public class ProductController { @Autowired private ProductService prodService; @RequestMapping("/list") public String list(String queryString, String catalog_name, String
price, Integer page, String sort, Model model) throws Exception { ResultModel
resultModel = prodService.search(queryString, catalog_name, price, page, sort); // 搜索结果返回 model.addAttribute("result", resultModel); // 回显查询条件 model.addAttribute("queryString", queryString); model.addAttribute("catalog_name", catalog_name); model.addAttribute("price", price); model.addAttribute("sort", sort); return "product_list"; } } |