Solr是一种开放源码的、基于 Lucene Java 的搜索服务器,易于加入到 Web 应用程序中。Solr 支持XML、Json等多种格式的数据。也就是说,用户上传的文档集可以是整理成XML格式或者Json等格式的。Solr和Lucene的联系在于,Solr的底层核心技术是使用Lucene实现的。
在我的理解中, Solr在服务器上运行,用户预先爬好文档并且处理成XML格式(也就是说,Solr本身不包含数据爬取的部分),上传到服务器, Solr做的工作是将文档集建立索引。Solr建好索引之后,用户就可以输入词语进行查询了。
二、在Windows下配置Solr(基于jetty容器)
如果要将Solr安放在Tomcat服务器上,建议参考《跟益达学Solr5之使用Tomcat部署Solr》 传送→ http://iamyida.iteye.com/blog/2209106
但是按照我的经验来看,还是使用Solr安装包中自带的jetty容器比较方便。直接从官网下载Solr,解压。我选择的是4.10.0版本,解压后的文件夹为E:\ProgramFiles\solr-4.10.0。
运行cmd进入Windows下的命令行,进入以下文件夹:
E:\ProgramFiles\solr-4.10.0\example
执行java -jar start.jar,启动Solr,如图:
访问http://localhost:8983/solr/,出现如下图界面说明Solr启动成功:
第一次运行起solr之后里面是没有任何数据文件的,我们首先要做的是导入搜索基于的文件集。为了简便起见,首先用solr自带的测试例子尝试导入。
三、新建一个core并且导入数据文件集
在本地文件夹中进入E:\ProgramFiles\solr-4.10.0\example\solr,新建一个文件夹core2,将E:\ProgramFiles\solr-4.10.0\example\multicore\core0(这是solr文件夹里自带的内容)中的conf文件夹复制到E:\ProgramFiles\solr-4.10.0\example\solr\core2中,同时在E:\ProgramFiles\solr-4.10.0\example\solr\core2中新建一个空文件夹data
进入http://localhost:8983/solr/,也就是solr的管理界面,点击管理页面的Core Admin, 添加一个名为core2的新core,注意要改掉name和instanceDir为core2,dataDir就不必改了,因为我们已经在本地文件夹中新建好data文件夹了。点击Add Core,如图:
注意,在管理界面Add core这一步骤必须在本地新建文件夹之后进行,否则Add core 会报错。
此时再打开本地文件夹,进入E:\ProgramFiles\solr-4.10.0\example\solr\core2,发现发生了一些变化,例如该文件夹下多了core.properties,并且data文件夹中多了index文件夹和tlog文件夹。
我们要准备好用于上传的数据文件集。这个文件集是通过对某个网站进行爬取得到的并且输出到一个xml文件(或者json文件中)。上传的数据文档集的格式必须与core2的配置文件E:\ProgramFiles\solr-4.10.0\example\solr\core2\conf\schema.xml相匹配。我们要首先观察准备好的要上传的文件,并且相应地修改schema.xml。
我准备的数据文件是个xml文档(已经做完了爬取网页并处理的部分),名为final.xml,内容大致如下:
我们可以看到,这个数据文档中有3个字段,url,title,p,分别对应爬下来的网页的url,标题,内容。
修改schema.xml,如图,原始的schema.xml为:
修改为如下图:
增加了<fieldType name="text_ik" class="solr.TextField">,是一个中文分词的设置。此时需要进入本地文件夹,将中文分词的包IKAnalyzer2012FF_u1.jar放在E:\ProgramFiles\solr-4.10.0\example\solr-webapp\webapp\WEB-INF\lib文件夹下。然后修改<!-- general -->下的前三项,name分别修改为数据文件中的三个字段,第二个和第三个因为会出现中文,因此type也需要修改为“text_ik”。<uniqueKey>设置为url,这是主键。<defaultSearchField>是要日后要被搜索的内容。
修改完schema.xml之后要重启一下solr,也就是要关掉当前solr运行的控制台窗口,重新进入E:\ProgramFiles\solr-4.10.0\example,运行java -jar start.jar。重启solr才能使刚才的配置生效。
理论上说使用solr文件夹中的post.jar 就可以用命令行上传数据集文件了。不过为了简便起见我使用了Solr 的管理界面。在管理界面中选中core2进行操作,进入Documents,将Document Type设置为xml,将final.xml中的文件复制到Document(s)中,然后点击Submit Document。如果文件比较大,这一步花费的时间可能会很长,耐心等待。如图:
至此,上传数据文件集的步骤就完成了。此时可以在管理界面进行搜索了。点击Query,在q中输入你要搜索的单词,先不用管下面的各种选项,点击最下方的Execute Query,就会显示搜索结果(以json的形式):
注意,网页上方的http://localhost:8983/solr/core2/select?q=%E4%BC%9A&wt=json&indent=true就是向服务器提交的查询语句。
至此,基本的solr的使用就结束了。
四、Solr在java中的使用
在Java中运用solr需要一个叫做solrj的jar包,但是最终我并没有采取这种方式,对于我的小项目,用了更为简单轻量的直接利用爬虫的原理向服务器提交请求。这里仅展示代码以显示这样做是可行的。
核心代码:
importjava.io.IOException;
importjava.util.Iterator;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
public class SolrJTest {
public void query(){
String url="http://127.0.0.1:8983/solr/";
HttpSolrClient hsc = new HttpSolrClient.Builder(url).build();
SolrQuery sq = new SolrQuery();
sq.set("q", "p:\" \"");
sq.setStart(0);
sq.setRows(10);
try {
QueryResponse resp = hsc.query("core0",sq);
SolrDocumentList res = resp.getResults();
System.out.println(res.getNumFound());
System.out.println(resp.getQTime());
for(Iterator<SolrDocument> i=res.listIterator();i.hasNext();){
SolrDocument sd=i.next();
System.out.print("url:"+sd.getFieldValue("url"));
System.out.print("\ntitle:"+sd.getFieldValue("title"));
System.out.println("\np:"+sd.getFieldValue("p"));
}
} catch (SolrServerException | IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
SolrJTesttest=new SolrJTest();
test.query();
}
}
五、构建一个web搜索引擎
利用管理平台使用solr仅仅是第一步,更重要的是我们要在java代码中实现一些内容,从而在web开发中利用jsp使用solr。jsp要做的工作就是提交关于搜索内容的请求到solr的服务器,并且将solr服务器返回的内容(json格式)解析,得到想要的结果以呈现在前段上。
这一部分的原理实际上和爬虫的原理是一样的。都是向服务器发送请求,然后服务器返回结果。
核心代码:
建立连接:
importjava.io.BufferedReader;
importjava.io.InputStreamReader;
importjava.net.URL;
importjava.net.URLConnection;
public class pachong {
public String paqu(String url0) {
// 定义即将访问的链接
String url = url0;
// 定义一个字符串用来存储网页内容
String result = "";
// 定义一个缓冲字符输入流
BufferedReader in = null;
try {
// 将string转成url对象
URL realUrl = new URL(url);
// 初始化一个链接到那个url的连接
URLConnection connection = realUrl.openConnection();
// 开始实际的连接
connection.connect();
// 初始化BufferedReader输入流来读取URL的相应
in = newBufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8"));
// 用来临时存储抓取到的每一行数据
String line;
while ((line = in.readLine()) != null) {
// 遍历抓取到的每一行并将其存储到result里面
result += line + "\n";
}
} catch (Exception e) {
System.out.println("发送GET请求出现异常!" + e);
e.printStackTrace();
} // 使用finally来关闭输入流
finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return result;
}
}
向服务器提出请求并获取结果到String中:
newpachong().paqu("http://localhost:8983/solr/core1/spellcheck?spellcheck.build=true");
String XmlContext = new pachong().paqu("http://localhost:8983/solr/core1/spellcheck?q=" + t + "&rows=0");
六、搜索引擎具体功能
1. 爬虫
爬虫利用了一个叫做Scrapy的Python爬虫框架,并且使用XPath来从页面的HTML源码中选择需要提取的数据。
文件目录如下:
Scrapy爬取下来的内容是以json格式的存储的,为了方便后面,我往Scrapy里添加了一段将json格式的文档转换成xml格式的文档的代码,爬下来的内容主要有url、标题、内容、发布时间、评论数。
核心Python代码如下:
#coding=utf-8
import scrapy
import time
import random
fromscrapy.spiders import Spider
fromscrapy.selector import Selector
from bcexo.itemsimport BcexoItem
classSportsSpider(scrapy.Spider):
name = "sina"
allowed_domains = ["sina.com.cn"]
start_urls = [
#"http://sports.sina.com.cn/g/premierleague/"
"http://sports.sina.com.cn/"
#"http://sports.sina.com.cn/basketball/nba/2017-05-20/doc-ifyfkqiv6579321.shtml"
]
def parse(self, response):
cur_url = response.url
sel = Selector(response)
links = sel.xpath('//a[@href]')
if "/doc" in cur_url:
item = BcexoItem()
item['link'] = cur_url
item['title'] =sel.css('.article-a__title::text').extract()[0]#xpath('//div[@class="article-a__title"]//text()').extract()[0]#
item['desc'] = ""
content =sel.css('.article-a__content p::text').extract()
for ct in content:
item['desc'] = item['desc'] +ct
item['date'] =sel.css('.article-a__time::text').extract()[0]
item['count'] =str(random.randint(0,1000))
#print("link:\n%s\ntitle:\n%s\ndesc:\n%s\ntime:\n%s\ncount:\n%s\n"%(item['link'],item['title'],item['desc'],item['date'],item['count']))
#time.sleep(30)
yield item
for href in links:
link =str(href.re('href="(.*?)"')[0])
try:
if link:
if notlink.startswith('http'): # 处理相对URL
link = cur_url + link
yield scrapy.Request(link,callback=self.parse)
except Exception as e:
print("!!")
搜索引擎的核心部分利用了solr框架。最前面已经讲过,Solr主要的作用是建立帮助建立索引。在jetty容器中运行起来solr,需要自己添加中文分词。将刚才得到的文档集xml传给solr就会建立倒排表索引。Solr的倒排表索引包括文档频次(多少文档出现过这个Term)和词频(某个文档中该Term出现过几次)。查询的时候向solr所在的服务器发送一个请求,solr就会返回搜索结果。Solr返回的结果是json格式(当然也可以是xml格式)的,需要自己写代码对json进行解析。
2. 文档排序
我写的给文档打分参考的字段依次是文章内容、标题、发布时间、评论数。这相当于不同的优先级。只有基于文章内容打分获得了平局的情况下才会考虑依据标题打分,依据标题平局了的情况下才会依据发布时间。事实上,如果修改代码将发布时间放在第一个,搜索结果的排序几乎是严格的时间序。
核心代码:
String JsonContext = new pachong().paqu("http://localhost:8983/solr/core1/select?q=" + t
+ "&sort=desc+desc%2Ctitle+desc%2Cdate+desc%2Ccount+desc&wt=json&indent=true");
String Initialpart = JsonContext.substring(JsonContext.indexOf("docs") + 6, JsonContext.length() - 4);
System.out.println(Initialpart);
String Highlight = JsonContext.substring(JsonContext.indexOf("highlighting") + 14, JsonContext.length() - 2);
JSONObject highlight = JSONObject.fromObject(Highlight);
JSONArray jsonArray = JSONArray.fromObject(Initialpart);
3. 高亮显示
Solr可以标记你搜索的单词。利用这个实现高亮。这个标记功能就是除了正常返回的结果,再多返回一些结果,这些结果就是我们所要的包含高亮搜索词语的内容。搜索词语被用特殊的标签标记了一下。也就是说,这部分内容和正常的结果是分别返回的,需要对利用这部分内容对正常结果进行替换拼接,还要自己写代码设置一下高亮的形式,是加粗还是红色。
参数说明:
l hl.fl: 用空格或逗号隔开的字段列表。要启用某个字段的highlight功能,就得保证该字段在schema中是stored。如果该参数未被给出,那么就会高亮默认字段 standard handler会用df参数,dismax字段用qf参数。你可以使用星号去方便的高亮所有字段。如果你使用了通配符,那么要考虑启用hl.requiredFieldMatch选项。
l hl.requireFieldMatch: 如果置为true,除非用hl.fl指定了该字段,查询结果才会被高亮。它的默认值是false。
l hl.usePhraseHighlighter: 如果一个查询中含有短语(引号框起来的)那么会保证一定要完全匹配短语的才会被高亮。
l hl.highlightMultiTerm :如果使用通配符和模糊搜索,那么会确保与通配符匹配的term会高亮。默认为false,同时hl.usePhraseHighlighter要为true。
l hl.fragsize: 返回的最大字符数。默认是100.如果为0,那么该字段不会被fragmented且整个字段的值会被返回。
注意事项:
一定要设置主键!
solr对高亮的设计是,高亮部分跟结果集部分是分开返回的,如果没有配主键,那么高亮部分返回的结果是这样的,如下图所示,可以看出高亮部分没有带主键,这个时候,你就与上面的结果集匹配不上,那么这样的高亮就没有任何意义,因为不能够确定高亮的是哪条记录。因为solr的结果集跟高亮是分开返回的,而且高亮是不会排序的,所以我把我的接口设计成,将高亮部分替换结果集的部分。
核心配置文件:
核心代码:
StringJsonContext = new pachong().paqu("http://localhost:8983/solr/core1/select?q=" + t
+ "&sort=desc+desc%2Ctitle+desc%2Cdate+desc%2Ccount+desc&wt=json&indent=true");
String Initialpart = JsonContext.substring(JsonContext.indexOf("docs") + 6, JsonContext.length() - 4);
System.out.println(Initialpart);
String Highlight = JsonContext.substring(JsonContext.indexOf("highlighting") + 14, JsonContext.length() - 2);
JSONObject highlight = JSONObject.fromObject(Highlight);
JSONArray jsonArray = JSONArray.fromObject(Initialpart);
int size = jsonArray.size();
String ans = "";
for (int i = 0; i < size; i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
String jsonurl = "" + jsonObject.get("link");
Object high_object = highlight.get(jsonurl);
JSONArray array1 = JSONArray.fromObject(JSONObject.fromObject(high_object).get("desc"));
JSONArray array2 = JSONArray.fromObject(JSONObject.fromObject(high_object).get("title"));
String high_content1 = array1.getString(0);
String high_content2 = array2.getString(0);
ans += "[" + i + "]link=" + jsonObject.get("link") + "\n&";
ans += "[" + i + "]desc=" + high_content1 + "\n&";
Result result;
if (high_content2 == "null") {
ans += "[" + i + "]title=" + jsonObject.get("title") + "\n&";
result = new Result("" + high_content1, "" + jsonObject.get("link"), "" + jsonObject.get("title"),
suggestion);
} else {
ans += "[" + i + "]title=" + high_content2 + "\n&";
result = new Result("" + high_content1, "" + jsonObject.get("link"), "" + high_content2, suggestion);
}
r.add(result);
}
return ans;
4. 相关搜索
这一部分并不是爬网页的时候从网页上爬下来的。每当我搜索一个词在文档集中查找匹配的文档时,同时还做另一个工作:将搜索词与索引进行比对,找到那些与搜索词相近的索引中的词。具体是调用了一个计算搜索词与索引词距离的方法,从而找到“相关搜索”。
很好的参考文献:
http://www.cnblogs.com/HD/p/3993424.html
核心配置文件:
核心代码:
// 相关搜索
String[] suggestion = null;
new pachong().paqu("http://localhost:8983/solr/core1/spellcheck?spellcheck.build=true");
String XmlContext = new pachong().paqu("http://localhost:8983/solr/core1/spellcheck?q=" + t + "&rows=0");
XmlContext = XmlContext.substring(XmlContext.indexOf("suggestions"), XmlContext.indexOf("</response>"));
if (XmlContext.length() >= 25) {
// System.out.println(XmlContext);
String numFound = XmlContext.substring(XmlContext.indexOf("numFound") + 10, XmlContext.indexOf("</int>"));
// System.out.println(numFound);
suggestion = new String[Integer.parseInt(numFound)];
for (int i = 0; i < Integer.parseInt(numFound); i++) {
XmlContext = XmlContext.substring(XmlContext.indexOf("<str>") + 5, XmlContext.length());
suggestion[i] = XmlContext.substring(0, XmlContext.indexOf("</str>"));
// System.out.println(suggestion[i]);
}
}
JSP后台与前端的交互:
request.setCharacterEncoding("utf-8");
Stringsearchcontent = request.getParameter("search_text").trim();
ArrayList<Result>r;
//searchProcess sp = newsearchProcess();
//sp.setSentence(searchcontent);
//r = sp.getResult();
String str =Test.getURLEncoderString(searchcontent);
//out.print("abc");
Test test = new Test();
if(searchcontent.indexOf("time_search")!=-1){
Stringsearchword=searchcontent.substring(0,searchcontent.indexOf("time_search")-1);
Stringsearchtime=searchcontent.substring(searchcontent.indexOf("time_search")+12,searchcontent.indexOf("time_search")+20);
//out.print(searchword);
//out.print(searchtime);
searchcontent=searchcontent.substring(0,searchcontent.indexOf("time_search")-1);
str =Test.getURLEncoderString(searchcontent);
Stringtime_str = Test.getURLEncoderString(searchtime);
Stringtime_information = test.time_display(str,time_str);
}else{
Stringinformation = test.display(str);
}
r =test.getResult();
if (r.get(0).getSuggest() != null) {
String[]suggestion = r.get(0).getSuggest();
out.print("相关搜索: ");
for (int j = 0; j < suggestion.length; j++) {
out.print("<a href='/SportsSearch/main.jsp?search_text=" + suggestion[j]
+"'style='font-size:13px;''>" + suggestion[j] + "</a>");
out.print(" ");
}
}
5. 按时间group搜索结果并返回
实际就是在向solr服务器提出请求的时候多加一个时间的限制,solr会在满足这个限制的条件下返回排序前10位的文档。比如我只想看到2017年的所有新闻。
核心代码:
StringJsonContext = new pachong().paqu("http://localhost:8983/solr/core1/select?q=" + t
+ "&fq=date:"+time+"*&wt=json&indent=true");
String Initialpart = JsonContext.substring(JsonContext.indexOf("docs") + 6, JsonContext.length() - 4);
//System.out.println(JsonContext);
String Highlight = JsonContext.substring(JsonContext.indexOf("highlighting") + 14, JsonContext.length() - 2);
JSONObject highlight = JSONObject.fromObject(Highlight);
JSONArray jsonArray = JSONArray.fromObject(Initialpart);
int size = jsonArray.size();
// System.out.println("Size: " +size);
String ans = "";
for (int i = 0; i < size; i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
String jsonurl = "" + jsonObject.get("link");
Objecthigh_object = highlight.get(jsonurl);
JSONArray array1 = JSONArray.fromObject(JSONObject.fromObject(high_object).get("desc"));
JSONArray array2 = JSONArray.fromObject(JSONObject.fromObject(high_object).get("title"));
String high_content1 = array1.getString(0);
String high_content2 = array2.getString(0);
// System.out.println(high_content2);
ans += "[" + i + "]link=" + jsonObject.get("link") + "\n&";
// ans += "[" + i +"]title=" + jsonObject.get("title")+"\n&";
// ans += "[" + i +"]p=" + jsonObject.get("p")+"\n&";
ans += "[" + i + "]desc=" + high_content1 + "\n&";
Result result;
if (high_content2 == "null") {
ans += "[" + i + "]title=" + jsonObject.get("title") + "\n&";
result = new Result("" + high_content1, "" + jsonObject.get("link"), "" + jsonObject.get("title"),
suggestion);
} else {
ans += "[" + i + "]title=" + high_content2 + "\n&";
result = new Result("" + high_content1, "" + jsonObject.get("link"), "" + high_content2, suggestion);
//System.out.println(high_content2);
}
r.add(result);
}
return ans;
JSP后台与前端交互的代码处理:
out.print("时间: ");
for(int k=0;k<5;k++){
out.print("<ahref='/SportsSearch/main.jsp?search_text=" + str
+"%20time_search=2017年0"+(k+1)+"月 'style='font-size:13px;''>2017年" + (k+1) + "月</a>");
out.print(" ");
}