先说说什么是solr,我从百度上复制了一点定义:
Solr是一个独立的企业级搜索应用服务器,它对外提供类似于Web-service的API接口。用户可以通过http请求,向搜索引擎服务器提交一定格式的XML文件,生成索引;也可以通过Http Get操作提出查找请求,并得到XML格式的返回,Solr是一个高性能,采用Java5开发,基于Lucene的全文搜索服务器。同时对其进行了扩展,提供了比Lucene更为丰富的查询语言,同时实现了可配置、可扩展并对查询性能进行了优化,并且提供了一个完善的功能管理界面,是一款非常优秀的全文搜索引擎。
多了我就不复制了,大家可以网上搜搜看,我这里把我如何用到solr,已经用到了solr中的哪些地方和如何使用展示出来,solr的功能非常强大,但是我用到的到不是很多。
下面我按照需求-思路-实现来讲解solr的使用,这是我平生第一次使用solr,不到之处还望多多指正:
需求原因:系统数据量太大,根据系统业务逻辑需要,sql已经没有多少优化空间,并发量太大和过大的数据量导致查询响应速度缓慢,页面加载缓慢,用户体验非常不好。
需求:系统原来的逻辑是:redis->DB,修改为redis->solr->DB
这样设计的原因:
1、首先同等并发量和数据量的情况下,solr检索出结果集的速度远大于关系型数据库(具体原因涉及到检索排序算法<solr是基于倒排序算法的> 等多种原因)
2、从solr服务器中读取数据,就避免了访问访问DB,除非solr服务器出现问题,这个概率不是很大
当然主要原因还是第一个,查询速度快,用户体验会上升
开发思路:既然系统要接入solr,自己首先得弄清楚原理,为什么solr可以当作数据库来使用。
solr是企业级搜索服务器,提供对数据的存储,数据索引创建等一系列功能。比如对一条数据创建索引的时候,同时将这个数据保存到solr服务器,就可以根据索引数据查询到整条数据,知道solr可以当作容器一样存放数据,除此之外,solr还提供了跟DB类似的功能-对数据的增删改查,知道这些,就知道为什么可以“替代”DB了吧。
代码开发阶段:
一、开发环境的搭建:
solr是独立的服务器,所以我们需要搭建环境,我使用的应用服务器是tomcat,将solr接到tomcat服务器中即可,至于怎么配置的,我这里不想详细说明了,网上配置这个东西的就像配置JDK环境变量一样多,大家自己动手,丰衣足食,我会上传一个我配置好solr功能的tomcat服务器,大家可以直接下载,地址:http://download.csdn.net/detail/duyunduzai/7286411
二、代码开发:
我代码中是将solr封装成一个接口,只需要调用对索引增删改查的接口即可。使用的是solrj系类功能,下面会针对代码详细讲述。
1、创建接口
/** * * 前面文章中说过了,是对系统的优化,假如之前数据库中保存的数据是用户的信息 * User:username,age,email,custNo; * */ public interface ISolrUserService { /** * 一句话功能描述:创建用户索引数据 */ public boolean createUserIndex(User user); /** * 一句话功能描述:批量创建用户索引数据 */ public boolean createUserIndex(List<User> userLists); /** * 一句话功能描述:批量删除用户索引数据 */ public boolean deleteUserIndex(List<String> custNos); /** * 一句话功能描述:查询用户数据 */ public QueryResult<SearchUserDTO> queryUser(SearchPage page); }
上面的代码是一个接口,其中包含的是对用户数据索引的创建、删除和查询,User类是用户信息类,包含四个属性:username,age,email,custNo,创建对应的实体类:
public class User { private String custNo; private String username; private String email; private int age; // 省略get和set方法 }
第一个代码片段中的QueryResult是我写的查询结果类,SearchPage是一个查询条件类,我先把代码贴出来给大家看看:
/** * * 功能描述: 查询结果基类 */ public class QueryResult<T> implements Serializable { private static final long serialVersionUID = 1L; private List<T> datas; private Boolean isLastPage; private Integer totalDataCount; private int pageNumber = 1; private int pageSize = 10; private Integer pageCount; private int indexNumber; /** * @param totalDataCount 总数据件数 * @param pageSize 每页显示条数 * @param pageNumber 当前的页数 */ public QueryResult(int totalDataCount, int pageSize, int pageNumber) { super(); this.totalDataCount = totalDataCount; this.pageSize = pageSize; this.pageNumber = pageNumber; if (this.pageNumber < 1) { this.pageNumber = 1; } if (this.totalDataCount <= 0) { return; } // 如果查询页数大于总页数,则取最后一页 if (this.totalDataCount <= (this.pageNumber - 1) * this.pageSize) { this.pageNumber = (this.totalDataCount + this.pageSize - 1) / this.pageSize; } this.indexNumber = (this.pageNumber - 1) * this.pageSize; // 总页数 this.pageCount = (this.totalDataCount + this.pageSize - 1) / this.pageSize; // 是否为最后一页 this.isLastPage = (this.pageNumber == this.pageCount ? true : false); } public QueryResult() { super(); } /** * 返回的数据集 * @return the datas */ public List<T> getDatas() { return datas; } /** * @param datas the datas to set */ public void setDatas(List<T> datas) { this.datas = datas; } /** * 满足查询条件的总记录数, null 意味着未知。注:只在查询第一页时返回正确的总记录数,其它页码时,返回-1 * @return the totalDataCount */ public Integer getTotalDataCount() { return totalDataCount; } /** * @param totalDataCount the totalDataCount to set */ public void setTotalDataCount(Integer totalDataCount) { this.totalDataCount = totalDataCount; } /** * 页码,从1开始 * @return the pageNumber */ public int getPageNumber() { return pageNumber; } /** * @param pageNumber the pageNumber to set */ public void setPageNumber(int pageNumber) { this.pageNumber = pageNumber; } /** * 满足查询条件的总页数, null 意味着未知。注:只在查询第一页时返回正确的总记录数,其它页码时,返回-1 * * @return the pageCount */ public Integer getPageCount() { return pageCount; } /** * @param pageCount the pageCount to set */ public void setPageCount(Integer pageCount) { this.pageCount = pageCount; } /** * 每页大小,缺省为10条记录/页 * @return the pageSize */ public int getPageSize() { return pageSize; } /** * @param pageSize the pageSize to set */ public void setPageSize(int pageSize) { this.pageSize = pageSize; } /** * 标志是否最后一页,True: 是最后一页,False: 不是,null:未知 * @return the lastPage */ public Boolean getIsLastPage() { return isLastPage; } /** * @param lastPage the lastPage to set */ public void setIsLastPage(Boolean lastPage) { this.isLastPage = lastPage; } /** * 计算开始数 * @return the lastPage */ public int getIndexNumber() { return indexNumber; } }
上面是查询结果基类,还有一个类是查询条件类
public class SearchPage implements Serializable { private static final long serialVersionUID = 1L; /** 页码 */ private int pageNumber = 1; /** 每页记录数 */ private int pageSize = 10; /** 总记录数 */ private int totalCount; /** 排序字段 */ private String[] orderType; /** * 查询关键字 */ private String keyword; /** 多条件查询 */ private String selectParam; /** 默认查询字段 */ private String field; private int Start = 0; // 省略get和set }
上面贴了这么多代码,其实都是跟solr无关的代码,不过可以完整展现我的代码逻辑,方便大家给我指正
接下来是对isolrUserService接口的实现类,在实现类中就是具体的实现如何对数据创建索引,删除索引等操作
@Service public class SolrUserServiceImpl implements IsolrUserService { private static final Logger LOGGER = LoggerFactory.getLogger(SolrUserServiceImpl.class); private static HttpSolrServer httpSolrServer; /** httpServer 是用来连接solr服务器,这里采用单例模式设计 */ private static HttpSolrServer getHttpSolrServer() { if (httpSolrServer == null) { /** 用户(User)数据solr服务地址 */ httpSolrServer = new HttpSolrServer("http://10.22.10.110:8983/solr/user"); /** 设置solr查询超时时间 */ httpSolrServer.setSoTimeout(1000); /** 设置solr连接超时时间 */ httpSolrServer.setConnectionTimeout(1000); /** solr最大连接数 */ httpSolrServer.setDefaultMaxConnectionsPerHost(1000); /** solr最大重试次数 */ httpSolrServer.setMaxRetries(1); /** solr所有最大连接数 */ httpSolrServer.setMaxTotalConnections(100); /** solr是否允许压缩 */ httpSolrServer.setAllowCompression(false); /** solr是否followRedirects */ httpSolrServer.setFollowRedirects(true); } return httpSolrServer; } @Override public boolean createUserIndex(User user) { // 获取solr服务 SolrServer solrServer = getHttpSolrServer(); try { // 创建索引,因为solr创建索引的时候,在参数类中的属性上面需要注解@Field, //所以,要将user类转换成可以创建索引的类,我单独创建了一个类,对应User, // SearchUserDTO.java,跟User类属性一样,只是在各个属性上面添加@Field注解 solrServer.addBean(toSearchUser(user)); // 提交创建。就相当于DB中的commit solrServer.commit(); return true; } catch (IOException e) { LOGGER.error("", e); } catch (SolrServerException e) { LOGGER.error("", e); } return false; } private SearchUserDTO toSearchUser(User user) { SearchUserDTO userDTO = new SearchUserDTO(); // 此方法是将user中属性的值复制到userDTO属性,这个方法是复制类中属性名一样的属性值 BeanUtils.copyProperties(user, userDTO); return userDTO; } @Override public boolean createUserIndex(List<User> users) { if (users != null && users.size() > 0) { List<SearchUserDTO> datas = new ArrayList<SearchUserDTO>(users.size()); for (User user : users) { datas.add(toSearchUser(user)); } SolrServer solrServer = getHttpSolrServer(); try { // 批量创建评价回复索引数据 solrServer.addBeans(datas); solrServer.commit(); return true; } catch (IOException e) { // 如果创建失败的话,可以回滚 // solrServer.collback(); LOGGER.error("", e); } catch (SolrServerException e) { LOGGER.error("", e); } } return false; } @Override public boolean deleteUserIndex(List<String> custNos) { SolrServer solrServer = getHttpSolrServer(); try { // 根据唯一性标识删除索引 solrServer.deleteById(custNos); // 删除该核下所有索引 // solrServer.delete("*:*"); solrServer.commit(); return true; } catch (IOException e) { LOGGER.error("", e); } catch (SolrServerException e) { LOGGER.error("", e); } return false; } @Override public QueryResult<SearchUserDTO> queryUser(SearchPage pager) { QueryResult<SearchUserDTO> queryResult = new QueryResult<SearchUserDTO>(); QueryResponse response = null; // 设置默认查询条件,格式为:field:keyword,比如:"custNo:1234" String searchParam = pager.getField() + ":" + pager.getKeyword(); SolrServer server = getHttpSolrServer(); SolrQuery query = new SolrQuery(searchParam); // 设置限制条件查询,假如同时查询username为zhangsan的用户,这里查询条件格式我就暂不多说了,下面会和配置文件一起来说一下 // query.setFilterQueries("username:zhangsan"); query.setFilterQueries(pager.getSelectParam()); query.setStart(pager.getStart()); // 起始位置,用于分页,solrj中默认是每页10条数据 query.setRows(pager.getPageSize()); // 每页文档数 try { response = server.query(query); } catch (SolrServerException e) { LOGGER.error("查询索引出现问题", e); } if (response != null) { SolrDocumentList list = response.getResults(); List<SearchUserDTO> datas = new ArrayList<SearchUserDTO>(); setSearchUserDTOData(datas, list); queryResult.setTotalDataCount(new Long(list.getNumFound()).intValue()); queryResult.setPageNumber(pager.getPageNumber()); queryResult.setPageSize(pager.getPageSize()); queryResult.setDatas(datas); } return queryResult; } private void setSearchUserDTOData(List<SearchUserDTO> datas, SolrDocumentList list) { for (SolrDocument solrDocument : list) { SearchUserDTO userDTO = new SearchUserDTO(); // 根据属性名称从返回结果中取得数据,并且封装到返回对象中 SearchUserDTO.setUsername(solrDocument.getFieldValue("username").toString()); SearchUserDTO.setEmail(solrDocument.getFieldValue("email").toString()); SearchUserDTO.setCustNo(solrDocument.getFieldValue("custNo").toString()); SearchUserDTO.setAger((Integer)solrDocument.getFieldValue("age")); datas.add(SearchUserDTO); } } }
上面的代码是实现了对索引的创建,删除以及查询,对索引的更新操作其实就是对索引的重新创建,就像redis中创建键值时一样的,创建已存在的健的话新值就是把原来的值覆盖掉。大家肯定会有疑问,solr是如何把类的字段跟索引连接到一起的呢,其实这里面是有一个配置文件的,下面重点讲讲这个配置文件:
solr服务支持单核和多核,假如只是对一张表进行创建索引数据,只使用单核就可以了,但是如果你对用户表创建索引数据,同时又对商品信息表创建索引数据,就需要对这两个表的索引数据放在不同 的地址下面,这里就利用到了solr多核,我上传到tomcat-solr服务器就是多核配置
解压tomcat-solr,在../apache-tomcat-7.0.26-master\webapps\solr\conf\multicore\下面有一个solr.xml,这里配置的是多核信息,在user、productInfo表示两个核,这里以user为例,user文件夹下有两个文件夹,conf和data,其中data中存放的是索引文件,这个就不多说了,索引文件的格式,内容等是solrj创建索引的时候写到里面去的,说一下conf问价夹,conf下面有三个文件:solrconfig.xml,scheam.xml和dataimport.properties,其中solrconfig.xml和dataimport.properties是可以配置很多功能的,但是我这个例子中只需要用到schema.xml配置,其余两个用默认就行了,重点讲下schema.xml:
<?xml version="1.0" ?> <schema name="example core user" version="1.1"> <types> <fieldtype name="string" class="solr.StrField" sortMissingLast="true" omitNorms="true"/> <!--add IKAnalyzer configuration--> <fieldType name="textik" class="solr.TextField" > <analyzer type="index"> <tokenizer class="org.wltea.analyzer.solr.IKTokenizerFactory" isMaxWordLength="false" useSmart="false"/> </analyzer> <analyzer type="query"> <tokenizer class="org.wltea.analyzer.solr.IKTokenizerFactory" isMaxWordLength="false" useSmart="false"/> </analyzer> </fieldType> <fieldType name="int" class="solr.TrieIntField" precisionStep="0" positionIncrementGap="0"/> </types> <fields> <field name="username" type="textik" indexed="true" stored="true" multiValued="false" required="true"/> <field name="custNo" type="string" indexed="true" stored="true" multiValued="false" required="true" /> <field name="email" type="string" indexed="true" stored="true" multiValued="false" /> <field name="age" type="int" indexed="false" stored="true" multiValued="false" /> </fields> <!-- field to use to determine and enforce document uniqueness. --> <uniqueKey>custNo</uniqueKey> <!-- field for the QueryParser to use when an explicit fieldname is absent --> <defaultSearchField>username</defaultSearchField> <!-- SolrQueryParser configuration: defaultOperator="AND|OR" --> <solrQueryParser defaultOperator="OR"/> </schema>
这里面配置的是一些字段信息,从上到下依次是types,fields,uniquekey,defaultSearchField,solrQueryParser,还有其他的一些配置的,我们这里没用到而已
types:这里面配置的是字段类型,就像java中的int,long,String等,用fieldType标签表示,以String为例:
<fieldtype name="string" class="solr.StrField" sortMissingLast="true" omitNorms="true"/>
name:FieldType的名称
class:指向org.apache.solr.analysis包里面对应的class名称,用来定义这个类型的行为
sortMissingLast:设置成true没有该field的数据排在有该field的数据之后,而不管请求时的排序规则, 默认是设置成false。 sortMissingFirst 跟上面倒过来呗
omitNorms:true 则字段检索时被省略相关的规范
fields:
name:字段名称
type:类型,此处写的类型必须在fieldType中声明,假如你type=long,在fieldType中就要有对应的long的声明(<fieldtype name="long" class="solr.LongField" omitNorms="true"/>),不然启动服务时会报错
indexed:是否创建索引,true表示创建,默认false,加入你username的indexed=false,那么你用username来查询索引是查询不到的
stored:是否保存数据,如果false的话,就查询返回结果中该字段数据就没有,这个属性在设计的时候是要注意的,有写字段不需要的话可以设为false,可以减小索引文件的大小
multiValued:是否多值,true的话就是多值,假如username的multiValued=true,在索引文件中查出的结果就是username['zhangsan','lisi'..]
required:是否必须,假如username的required=true,那么在创建索引的时候,username就必须有值,否则创建不成功
当然,field还包含其他很多属性,比如默认值defaule,是否压缩compressed等就不多写了,因为我没用到。
uniqueKey:唯一性主键
defaultSearchField:默认使用该字段查询,但是我们往往查询的时候是根据自己需要查询的,比如我们查询条件为:username:zhangsan,如果直接写成zhangsan的话,查询条件就是:"defauleSearchField:zhangsan"
solrQueryParser:设置默认操作,OR还是AND,加入我们设置<solrQueryParser defaultOperator="OR"/>,我们查询username:zhangsan lisi ,就相当于username=zhangsan OR username=lisi,如果这个地方我们设置成and的话,就是username=zhagnsan and username=lisi
配置文件这里就讲完了,现在大家应该懂了吧,下面加一点查询扩展
/*设置查询条件 *查询所有:*:* *按照名称查询:username:zhangsan *按照email查询:email:[email protected] *我们假设按照名称查询 */ SolrQuery query = new SolrQuery("username:zhangsan"); /* *设置限制级查询条件 *假如我们同时查出email是[email protected]的 * *这里面也可以多条件,假如email是[email protected]同时年龄是21 *就应该写成:email:[email protected] AND age:21(注意这里面的AND和OR必须大写) */ query.setFilterQueries("email:[email protected] AND age:21"); /* *设置排序 *假如要求按照名称降序 */ query.addSortField("username", ORDER.desc); query.addSortField(username, order); query.setStart(pager.getStart()); // 起始位置,用于分页 query.setRows(pager.getPageSize()); // 每页文档数 response = server.query(query);
// 概括一下上面的查询条件就是:username=zhangsan,age=21 [email protected],按照username降序排列(上面age的indexed=false,应该改为true,才能按照age索引)
根据这些,基本上可以满足对数据的条件查询了,如果是多表关联查询的话,我现在只知道从一个索引文件中查到数据,然后根据标识去另一个索引中查找,没发现有可以替代DB中的join表的功能,solr中还有很多功能,有高亮显示查询结果,根据各种复杂的算法解析啊等,第一次接触solr,就写到这儿吧,望各位指出不足之处