当前位置: 首页 > 工具软件 > solr_pager > 使用案例 >

solr的基本使用

宓博实
2023-12-01

最近负责的系统要接入solr,原因嘛,就是那些前辈们以前对数据库的操作,比如全表扫描,循环全表扫描等,随着库中数据的增大,导致系统动不动就罢工,全部改动需要时间,紧急解决方案就是用solr部分替代原数据库。。。

先说说什么是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://127.0.0.1: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:1234@sina.cn
	*我们假设按照名称查询
	*/ 
	SolrQuery query = new SolrQuery("username:zhangsan");
        /*
	*设置限制级查询条件
	*假如我们同时查出email是1234@sina.cn的
	*
	*这里面也可以多条件,假如email是1234@sina.cn同时年龄是21
	*就应该写成:email:1234@sina.cn AND age:21(注意这里面的AND和OR必须大写)
	*/
        query.setFilterQueries("email:1234@sina.cn 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=1234@sina.cn,按照username降序排列(上面age的indexed=false,应该改为true,才能按照age索引)

根据这些,基本上可以满足对数据的条件查询了,如果是多表关联查询的话,我现在只知道从一个索引文件中查到数据,然后根据标识去另一个索引中查找,没发现有可以替代DB中的join表的功能,solr中还有很多功能,有高亮显示查询结果,根据各种复杂的算法解析啊等,第一次接触solr,就写到这儿吧,望各位指出不足之处




 类似资料: