Lucene搜索引擎+HDFS+MR完成垂直搜索
介于上一篇的java实现网络爬虫基础之上,这一篇的思想是将网络收集的数据保存到HDFS和数据库(Mysql)中;然后用MR对HDFS的数据进行索引处理,处理成倒排索引;搜索时先用HDFS建立好的索引来搜索对应的数据ID,根据ID从数据库中提取数据,呈现到网页上。
这是一个完整的集合网络爬虫、数据库、HDFS、MapReduce、DAO设计模式、JSP/Servlet的项目,完成了数据收集、数据分析、数据索引并分页呈现。
完整的代码呈现,希望认真仔细阅读。
------>
目录:
1、搜索引擎阐述
2、数据库表建立
3、使用DAO设计模式进行数据库操作
【Ⅰ】数据库连接类DataBaseConnection
【Ⅱ】表单元素的封装类News
【Ⅲ】编写DAO接口INewsDAO
【Ⅳ】DAO接口的实现类NewsDAOImp类
【Ⅴ】工厂类DAOFactory类
4、网络爬虫实现★★ 【参考博客《java实现网络爬虫》和《Heritrix实现网络爬虫》】
5、MR(MapReduce)对HDFS数据进行索引处理★★
6、实现搜索引擎
【Ⅰ】创建web项目,编写测试用例,测试是否可以读取HDFS的数据内容
【Ⅱ】 编写index首页
【Ⅲ】处理HDFS查询的操作
【Ⅳ】servlet类搜索结果向页面传递
【Ⅴ】结果呈现,实现分页
7、总结
------>
1、搜索引擎阐述
搜索引擎的执行流程:
1) 通过爬虫来将数据下载到本地
2) 将数据提取出来保存到HDFS和数据库中(MySQL)
3) 通过MR来对HDFS的数据进行索引处理,处理成为倒排索引
4) 搜索时先使用HDFS建立好的索引来搜索对应的数据ID,再根据ID来从MySQL数据库中提取数据的具体信息。
5) 可以完成分页等操作。
倒排索引对应的就是正排索引,正排索引指的就是MySQL数据库中id的索引。
而倒排索引的目的是可以根据关键字查询出该关键字对应的数据id。
这里就需要用到MySQL数据库,以及通过Java EE版的Eclipse来完成网站的开发。
为了开发起来更方便,我们这里使用MyEclipse来完成。
2、数据库表建立
先安装好MySQL数据库。
安装时,注意编码选择gbk。
通过控制台的mysql -u用户名 –p密码 即可登录mysql数据库。
之后使用show databases可以看到所有的数据库。
使用create database 可以建立一个新的库。
使用 use 库名 ,可以切换到另一个库。
使用show tables可以看到一个库下的所有表。
之后就可以通过普通的sql语句来建立表和进行数据的操作了。
在进行数据库操作时,企业开发中必定要使用DAO(Data Access Object)设计模式
组成如下:
1) DataBaseConnection:建立数据库连接
2) VO:与表对应的数据对象
3) DAO接口:规范操作方法
4) DAOImpl:针对DAO接口进行方法实现。
5) Factory:用来建立DAO接口对象。
首先根据需求,将数据库表建立出来,这里只需建立一个简单的news新闻表,用于存储网络上爬取得数据。
CREATE TABLE news (
id int primary key ,
title varchar(200) not null,
description text ,
url varchar(200)
);
3、使用DAO设计模式进行数据库操作
根据上述的DAO设计模式,我们需要编写相关操作类,来完成数据库的操作。
【Ⅰ】 数据库连接类DataBaseConnection,需要导入jar包:
package org.liky.sina.dbc; import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException; /**
* 连接数据库mysql的sina_news
* @author k04
*
*/
public class DataBaseConnection {
//此处可以试着两种表达加载类的方法
// private static final String DBORIVER="org.git.mm.mysql.Driver";
private static final String DBORIVER="com.mysql.jdbc.Driver";
private static final String DBURL="jdbc:mysql://localhost:3306/sina_news";
private static final String DBUSER="root";
private static final String DBPASSWORD="admin"; private Connection conn;
/**
* 创建数据库连接
* @return
*/
public Connection getConnection(){
try {
if(conn==null||conn.isClosed()){
//建立一个新的连接
Class.forName(DBORIVER);
conn=DriverManager.getConnection(DBURL, DBUSER, DBPASSWORD);
//System.out.println("success to connect!");
}
}catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return conn;
}
/*
* 关闭连接
*/
public void close(){
if(conn!=null){
try{
conn.close();
}catch(SQLException e){
e.printStackTrace();
}
}
} }
【Ⅱ】表单元素的封装类News,根据建表时设定的四种元素id,title,description,url,将网络爬取得内容完整的导入数据库中,此处可以用shift+Alt+S在Eclipse快捷创建封装类:
package org.liky.sina.vo;
/**
* news封装类
* @author k04
*
*/
public class News {
private Integer id;
private String title;
private String description;
private String url; public News() {
}
public News(Integer id,String title,String description,String url) {
this.id=id;
this.title=title;
this.description=description;
this.url=url;
} public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
【Ⅲ】编写DAO接口INewsDAO,存放数据库操作类的方法名:
package org.liky.sina.dao;
/**
* 接口,呈现三个方法
*/
import java.util.List;
import org.liky.sina.vo.News; public interface INewsDAO {
/**
* 添加数据
* @param news 要添加的对象
* @throws Exception
*/
public void doCreate(News news)throws Exception;
/**
* 根据主键id查询数据
*
*/
public News findById(int id)throws Exception;
/**
* 根据一组id查询所有结果
* @param ids 所有要查询的id
* @return 查询到的数据
* 因为索引是根据热词查到一堆的id
*/
public List<News> findByIds(int[] ids)throws Exception; }
【Ⅳ】DAO接口的实现类NewsDAOImp类:
package org.liky.sina.dao.impl;
/**
* 继承INewsDAO接口
* 实现三个方法,插入数据,查找指定id数据,查找一组id数据
*/
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List; import org.liky.sina.dao.INewsDAO;
import org.liky.sina.dbc.DataBaseConnection;
import org.liky.sina.vo.News; public class NewsDAOImpl implements INewsDAO {
//声明一个数据库连接类对象
private DataBaseConnection dbc; //构造器,参数为数据库连接类对象
public NewsDAOImpl(DataBaseConnection dbc) {
this.dbc=dbc; } @Override
public void doCreate(News news) throws Exception {
// TODO Auto-generated method stub
String sql="INSERT INTO news (id,title,description,url) VALUES (?,?,?,?)";
PreparedStatement pst=dbc.getConnection().prepareStatement(sql);
//设置参数
pst.setInt(1, news.getId());
pst.setString(2, news.getTitle());
pst.setString(3, news.getDescription());
pst.setString(4, news.getUrl()); pst.executeUpdate();
System.out.println("create success.");
} @Override
public News findById(int id) throws Exception {
// TODO Auto-generated method stub
String sql="SELECT id,title,description,url FROM news WHERE id = ?";
PreparedStatement pst=dbc.getConnection().prepareStatement(sql);
pst.setInt(1, id);
ResultSet rs=pst.executeQuery();
News news=null;
//将符合id的数据遍历写入news并返回
if(rs.next()){
news=new News();
news.setId(rs.getInt(1));
news.setTitle(rs.getString(2));
news.setDescription(rs.getString(3));
news.setUrl(rs.getString(4));
}
//System.out.println("find success.");
return news;
} @Override
public List<News> findByIds(int[] ids) throws Exception {
// TODO Auto-generated method stub
StringBuilder sql=new StringBuilder("SELECT id,title,description,url FROM news WHERE id IN (");
//将id写入ids,并用逗号隔开
if(ids!=null&&ids.length>0){
for(int id:ids){
sql.append(id);
sql.append(",");
}
//截取最后一个逗号,并补上括号
String resultSQL=sql.substring(0, sql.length()-1)+")"; PreparedStatement pst=dbc.getConnection().prepareStatement(resultSQL);
ResultSet rs=pst.executeQuery();
//存取一组id到链表中
List<News> list=new ArrayList<>();
while(rs.next()){
News news=new News();
news.setId(rs.getInt(1));
news.setTitle(rs.getString(2));
news.setDescription(rs.getString(3));
news.setUrl(rs.getString(4));
list.add(news);
}
}
//System.out.println("find success.");
return null;
} }
【Ⅴ】工厂类DAOFactory类,此类写入了数据库连接类参数,返回DAO实现类对象:
java中,我们通常有以下几种创建对象的方式:
(1) 使用new关键字直接创建对象;
(2) 通过反射机制创建对象;
(3) 通过clone()方法创建对象;
(4) 通过工厂类创建对象。
package org.liky.sina.factory;
/**
* 工厂类
* 输入一个连接数据库对象的参数,返回数据库表操作的类
*/
import org.liky.sina.dao.INewsDAO;
import org.liky.sina.dao.impl.NewsDAOImpl;
import org.liky.sina.dbc.DataBaseConnection; public class DAOFactory {
public static INewsDAO getINewsDAOInstance(DataBaseConnection dbc){
return new NewsDAOImpl(dbc);
}
}
4、网络爬虫实现
现在编写整个项目的重点,编写URLDemo类,在爬虫中进行数据库的操作以及HDFS的写入:
a' 关于此类,在网页解析时用了简单的Jsoup,并没有如《java网络爬虫》用正则表达式,所以需要导入jsoup的jar包 ;
b' 关于HDFS在eclipse的配置以及本机的连接,我后续博客会阐述,也可以网络查询方法;
c' 这个类也是执行类,我收集的是新浪新闻网的数据,爬取深度为5,设置线程数5,并且筛选了只有链接含有“sian.news.com.cn”的。
d' 网络爬虫我讲了两种方法:(1)java代码实现网络爬虫
(2)Heritrix工具实现网络爬虫
此处我还是选择了直接写代码实现,自由度高也方便读写存取。
package org.liky.sina.craw; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set; import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.liky.sina.dao.INewsDAO;
import org.liky.sina.dbc.DataBaseConnection;
import org.liky.sina.factory.DAOFactory;
import org.liky.sina.vo.News; /**
* 爬虫开始进行数据库操作以及HDFS写入
*
* @author k04
*
*/
public class URLDemo {
// 该对象的构造方法会默认加载hadoop中的两个配置文件,hdfs-site.xml和core-site.xml
// 这两个文件包含访问hdfs所需的参数值
private static Configuration conf = new Configuration(); private static int id = 1; private static FileSystem fs; private static Path path; // 等待爬取的url
private static List<String> allWaitUrl = new ArrayList<>();
// 已经爬取的url
private static Set<String> allOverUrl = new HashSet<>();
// 记录所有url的深度,以便在addUrl方法内判断
private static Map<String, Integer> allUrlDepth = new HashMap<>();
// 爬取网页的深度
private static int maxDepth = 5;
// 声明object独享帮助进行线程的等待操作
private static Object obj = new Object();
// 设置总线程数
private static final int MAX_THREAD = 20;
// 记录空闲的线程数
private static int count = 0; // 声明INewsDAO对象,
private static INewsDAO dao; static {
dao = DAOFactory.getINewsDAOInstance(new DataBaseConnection());
} public static void main(String args[]) {
// 爬取的目标网址
String strUrl = "http://news.sina.com.cn/"; // 爬取第一个输入的url
addUrl(strUrl, 0);
// 建立多个线程
for (int i = 0; i < MAX_THREAD; i++) {
new URLDemo().new MyThread().start();
} // DataBaseConnection dc=new DataBaseConnection();
// dc.getConnection(); } public static void parseUrl(String strUrl, int depth) {
// 先判断当前url是否爬取过
// 判断深度是否符合要求
if (!(allOverUrl.contains(strUrl) || depth > maxDepth)) {
System.out.println("当前执行的 " + Thread.currentThread().getName()
+ " 爬虫线程处理爬取: " + strUrl); try {
// 用jsoup进行数据爬取
Document doc = Jsoup.connect(strUrl).get();
// 通过doc接受返回的结果
// 提取有效的title和description
String title = doc.title();
Element descE = doc.getElementsByAttributeValue("name",
"description").first();
String desc = descE.attr("content"); // System.out.println(title + " --> " + desc); // 如果有效,则惊醒保存
if (title != null && desc != null && !title.trim().equals("")
&& !desc.trim().equals("")) {
// 需要生成一个id,以便放入数据库中,因此id也要加入到HDFS中,便于后续索引
News news = new News();
news.setId(id++);
news.setTitle(title);
news.setDescription(desc);
news.setUrl(strUrl);
// 添加到数据库语句
dao.doCreate(news);
// 向HDFS保存数据
path = new Path("hdfs://localhost:9000/sina_news_input/"
+ System.currentTimeMillis() + ".txt");
fs = path.getFileSystem(conf);
FSDataOutputStream os = fs.create(path);
// 进行内容输出,此处需要用news.getId(),不然数据库和HDFS的id会不相同,因为多线程的运行
os.writeUTF(news.getId() + "\r\n" + title + "\r\n" + desc);
os.close(); // 解析所有超链接
Elements aEs = doc.getElementsByTag("a");
// System.out.println(aEs);
if (aEs != null && aEs.size() > 0) {
for (Element aE : aEs) {
String href = aE.attr("href");
System.out.println(href);
// 截取网址,并给出筛选条件!!!
if ((href.startsWith("http:") || href
.startsWith("https:"))
&& href.contains("news.sina.com.cn")) {
// 调用addUrl()方法
addUrl(href, depth + 1);
}
}
} } } catch (Exception e) { }
// 吧当前爬完的url放入到偶尔中
allOverUrl.add(strUrl);
System.out.println(strUrl + "爬去完成,已经爬取的内容量为:" + allOverUrl.size()
+ "剩余爬取量为:" + allWaitUrl.size()); // 判断是否集合中海油其他的内容需要进行爬取,如果有,则进行线程的唤醒
if (allWaitUrl.size() > 0) {
synchronized (obj) {
obj.notify();
}
} else {
System.out.println("爬取结束...");
System.exit(0);
} }
} /**
* url加入到等待队列中 并判断是否已经放过,若没有就放入allUrlDepth中
*
* @param href
* @param depth
*/
public static synchronized void addUrl(String href, int depth) {
// 将url放入队列中
allWaitUrl.add(href);
// 判断url是否已经存在
if (!allUrlDepth.containsKey(href)) {
allUrlDepth.put(href, depth + 1);
}
} /**
* 获取等待队列下一个url,并从等待队列中移除
*
* @return
*/
public static synchronized String getUrl() {
if (allWaitUrl.size() > 0) {
String nextUrl = allWaitUrl.get(0);
allWaitUrl.remove(0);
return nextUrl;
}
return null;
} /**
* 用多线程进行url爬取
*
* @author k04
*
*/
public class MyThread extends Thread { @Override
public void run() {
// 编写一个死循环,以便线程可以一直存在
while (true) {
// String url = getUrl();
if (url != null) {
// 调用该方法爬取url的数据
parseUrl(url, allUrlDepth.get(url));
} else {
System.out.println("当前线程准备就绪,等待连接爬取:" + this.getName());
// 线程+1
count++;
// 建立一个对象,帮助线程进入等待状态wait()
synchronized (obj) {
try {
obj.wait();
} catch (Exception e) {
e.printStackTrace();
}
// 线程-1
count--;
}
}
}
} } }
现在执行上述类URLDemo,该类执行流程:
1‘ 读取源链接,分配线程任务;
2’ 数据根据DAO设计模式写入到数据库中;
3‘ 数据上传到HDFS的input文件夹中;
执行完可以查看HDFS和数据库的结果:
关于此处,还是有些许瑕疵,双方的数据量并不相等,主要是多线程爬取的时候,id的传输并不相同!
将重复的数据进行清洗,
当数据爬取完成后,我们会发现有很多重复的数据,这可以通过SQL语句来进行清洗的操作。
SQL中可以使用group by关键字来完成分组函数的处理。
我们可以先通过该函数来测试一下。
测试后会发现真正有效数据量应该是4634条。
这就需要我们通过建立一张新表,把所有不重复的数据加入到新表中。
mysql支持create table时通过select语句来查询出一些结果作为新表的结构和数据。
CREATE TABLE new_news SELECT id,title,description,url FROM news WHERE id IN (SELECT min(id) FROM news GROUP BY title)
5、MR(MapReduce)对HDFS数据进行索引处理
下面就可以开始编写MR程序。
Map格式,要求key为关键字,value为id。
由于一个关键字可能会对应多个id,所以id之间我们想使用,来分隔。所以key和value的类型都应该是String(Text)
package org.liky.sina.index;
/**
* 编写MR程序。
Map格式,要求key为关键字,value为id。
由于一个关键字可能会对应多个id,所以id之间我们想使用,来分隔。所以key和value的类型都应该是String(Text)
*/
import java.io.IOException;
import java.util.*;
import java.util.regex.*;
import jeasy.analysis.MMAnalyzer;
import org.apache.hadoop.conf.*;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.*;
import org.apache.hadoop.mapred.*;
import org.apache.hadoop.util.*; public class IndexCreater extends Configured implements Tool { public static void main(String[] args) throws Exception {
21 int res = ToolRunner.run(new Configuration(), new IndexCreater(),
22 new String[] { "hdfs://localhost:9000/sina_news_input/",
23 "hdfs://localhost:9000/output_news_map/" });
System.exit(res);
} //Mapper接口中的后两个泛型参数,第一个表示返回后Map的key的类型,第二个表示返回后的value类型
public static class MapClass extends MapReduceBase implements Mapper<LongWritable,Text,Text,Text>{ private static Pattern p; static{
System.out.println("开始Map操作.....");
p=Pattern.compile("\\d+");
} private int id;
private int line=1; private static MMAnalyzer mm=new MMAnalyzer(); //输出的词
private Text word=new Text(); //map过程的核心方法
@Override
public void map(LongWritable key, Text value,
OutputCollector<Text, Text> output, Reporter reporter)
throws IOException {
if (line == 1) {
// 读取的是第一行,我们就需要将第一行的id保留下来
line++;
Matcher m = p.matcher(value.toString());
if (m.find()) {
id = Integer.parseInt(m.group());
}
} else {
String tempStr = value.toString();
// 按空格将单词拆分出来
// StringTokenizer itr = new StringTokenizer(line);
// 使用分词器来进行词组的拆分
String[] results = mm.segment(tempStr, "|").split("\\|");
// 每个单词记录出现了1次
for (String temp : results) {
word.set(temp.toLowerCase());
output.collect(word, new Text(id + ""));
}
} } } //对所有的结果进行规约,合并
//Reducer中也有泛型,前两个表示Map过程输出的结果类型,后两个表示Reduce处理后输出的类型
public static class Reduce extends MapReduceBase implements Reducer<Text,Text,Text,Text>{ static{
System.out.println("开始reduce操作.....");
}
@Override
public void reduce(Text key, Iterator<Text> values,
OutputCollector<Text, Text> output, Reporter repoter)
throws IOException {
//将所有key值相同的结果,求和
StringBuilder result=new StringBuilder();
while(values.hasNext()){
//存在一个key相同的,加入result
String temp=values.next().toString();
if(!result.toString().contains(temp+",")){
result.append(temp+",");
} }
//将其规约
output.collect(key, new Text(result.substring(0, result.length()-1)));
//输出key相同的id值
System.out.println(key+"---->"+result);
} } static int printUsage() {
System.out
.println("wordcount [-m <maps>] [-r <reduces>] <input> <output>");
ToolRunner.printGenericCommandUsage(System.out);
return -1;
} @Override
public int run(String[] args) throws Exception {
// TODO Auto-generated method stub
JobConf conf = new JobConf(getConf(), IndexCreater.class);
conf.setJobName("wordcount"); // 输出结果的Map的key值类型
conf.setOutputKeyClass(Text.class);
// 输出结果的Map的value值类型
conf.setOutputValueClass(Text.class); conf.setMapperClass(MapClass.class);
conf.setCombinerClass(Reduce.class);
conf.setReducerClass(Reduce.class); List<String> other_args = new ArrayList<String>();
for (int i = 0; i < args.length; ++i) {
try {
if ("-m".equals(args[i])) {
conf.setNumMapTasks(Integer.parseInt(args[++i]));
} else if ("-r".equals(args[i])) {
conf.setNumReduceTasks(Integer.parseInt(args[++i]));
} else {
other_args.add(args[i]);
}
} catch (NumberFormatException except) {
System.out.println("ERROR: Integer expected instead of "
+ args[i]);
return printUsage();
} catch (ArrayIndexOutOfBoundsException except) {
System.out.println("ERROR: Required parameter missing from "
+ args[i - 1]);
return printUsage();
}
}
// Make sure there are exactly 2 parameters left.
if (other_args.size() != 2) {
System.out.println("ERROR: Wrong number of parameters: "
+ other_args.size() + " instead of 2.");
return printUsage();
}
// 设置输出结果按照什么格式保存,以便后续使用。
conf.setOutputFormat(MapFileOutputFormat.class);
// 输入文件的HDFS路径
FileInputFormat.setInputPaths(conf, other_args.get(0));
// 输出结果的HDFS路径
FileOutputFormat.setOutputPath(conf, new Path(other_args.get(1))); JobClient.runJob(conf); return 0;
} }
6、实现搜索引擎
【Ⅰ】创建web项目,编写测试用例,测试是否可以读取HDFS的数据内容
在安装好的MyEclipse开发工具中,开始编写搜索引擎展示部分的内容。
这里先使用普通的JSP + Servlet的模式来完成程序的编写。
首先建立一个普通的Web项目。
之后,在里面编写一个测试用例,测试是否可以读取HDFS中的数据内容,注意需要先将hadoop/lib目录下的所有jar包,以及hadoop根目录下的支持jar包拷贝到项目WEB-INF目录下的lib目录中。
注意!!!完成上面数据的收集和分析之后,现在读取的内容需要从经过MR处理之后存储在HDFS的sina_new_ouputwenjianjia内读取。
package org.liky.sina.test; import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.MapFile.Reader;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapred.MapFileOutputFormat;
import org.junit.Test; public class TestCaseSina { @Test
public void test() throws Exception {
Configuration conf = new Configuration();
Path path = new Path("hdfs://localhost:9000/output_news_map/");
FileSystem fs = path.getFileSystem(conf);
Reader reader = MapFileOutputFormat.getReaders(fs, path, conf)[0];
Text value = (Text) reader.get(new Text("印度"), new Text()); System.out.println(value); }
}
【Ⅱ】 编写index首页
将之前写好的DAO代码也拷贝到项目中,以便以后查询数据库使用。
之后编写一个jsp页面,用来接收用户输入的查询关键字。
此处我就从简了 O(∩_∩)O
<body>
<center>
<form action="SearchServlet" method="post">
请输入查询关键字:
<input type="text" name="keyword">
<input type="submit" value="查询">
</form>
</center>
</body>
【Ⅲ】处理HDFS查询的操作
根据设置好的路径,建立一个SearchServlet,并完成doGet和 doPost方法。
在这个Servlet中会用到处理HDFS查询的操作方法,因此我们需要单独声明一个HDFSUtils工具类,来帮助我们实现查询的功能。
package org.liky.sina.utils; import java.io.IOException;
import java.util.Set;
import java.util.TreeSet; import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.MapFile.Reader;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapred.MapFileOutputFormat; public class HDFSUtils {
//连接hadoop的配置
private static Configuration conf = new Configuration();
//创建需要读取数据的hdfs路径
private static Path path = new Path(
"hdfs://localhost:9000/output_news_map/"); private static FileSystem fs = null; static {
try {
fs = path.getFileSystem(conf);
} catch (IOException e) {
e.printStackTrace();
}
} public static Integer[] getIdsByKeyword(String keyword) throws Exception { Reader reader = MapFileOutputFormat.getReaders(fs, path, conf)[0];
Text value = (Text) reader.get(new Text(keyword), new Text());
//set存放关键词搜索的一组id
Set<Integer> set = new TreeSet<Integer>();
String[] strs = value.toString().split(","); for (String str : strs) {
set.add(Integer.parseInt(str));
} return set.toArray(new Integer[0]);
} }
【Ⅳ】servlet类搜索结果向页面传递
在Servlet中通过调用HDFSUtils和之前写过的DAO方法,即可查询到结果并设置向页面传递。
此处将关键词搜索的结果呈现到result.jsp界面,所以下面就是编写该界面呈现最终结果。
package org.liky.sina.servlet; import java.io.IOException;
import java.util.List; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import org.liky.sina.dbc.DataBaseConnection;
import org.liky.sina.factory.DAOFactory;
import org.liky.sina.utils.HDFSUtils;
import org.liky.sina.vo.News; public class SearchServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doPost(request, response);
} public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 接收提交的查询关键字参数
// 先处理乱码
request.setCharacterEncoding("UTF-8");
// 接收参数
String keyword = request.getParameter("keyword");
// 根据关键字进行查询。
try {
Integer[] ids = HDFSUtils.getIdsByKeyword(keyword);
// 根据这些id来查询出相应的结果
List<News> allNews = DAOFactory.getINewsDAOInstance(
new DataBaseConnection()).findByIds(ids); // 将结果传递回页面显示
request.setAttribute("allNews", allNews); // 切换到页面上
request.getRequestDispatcher("/result.jsp").forward(request,
response);
} catch (Exception e) {
e.printStackTrace();
} }
}
【Ⅴ】结果呈现,实现分页
最后,我们需要在页面上将结果呈现出来,在web根目录下编写result.jsp文件。
可以通过JSTL + EL来完成内容的输出。
<%@page import="org.liky.sina.vo.News"%>
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://"
+ request.getServerName() + ":" + request.getServerPort()
+ path + "/";
%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>"> <title>新浪新闻搜索</title>
</head> <body>
<center>
<%
List<News> allNews = (List<News>)request.getAttribute("allNews");
%>
<table width="80%">
<%
for (News n : allNews) { %>
<tr>
<td>
<a href="<%=n.getUrl() %>" target="_blank"><%=n.getTitle() %></a> <br>
<%=n.getDescription() %>
<hr/>
</td>
</tr>
<%
} %>
</table> <%
int cp = (Integer)request.getAttribute("currentPage");
int allPages = (Integer)request.getAttribute("allPages");
%>
<form id="split_page_form" action="SearchServlet" method="post">
<input type="hidden" name="currentPage" id="cp" value="<%=cp %>" />
<input type="button" <%=cp == 1?"disabled":"" %> value="首页" onclick="changeCp(1);">
<input type="button" <%=cp == 1?"disabled":"" %> value="上一页" onclick="changeCp(<%=cp - 1 %>);">
<input type="button" <%=cp == allPages?"disabled":"" %> value="下一页" onclick="changeCp(<%=cp + 1 %>);">
<input type="button" <%=cp == allPages?"disabled":"" %> value="尾页" onclick="changeCp(<%=allPages %>);">
第 <%=cp %> 页 / 共 <%=allPages %> 页
<br>
请输入查询关键字:<input type="text" name="keyword" value="<%=request.getParameter("keyword")%>">
<input type="submit" value="查询">
</form>
<script type="text/javascript">
function changeCp(newcp) {
// 改变当前页数
document.getElementById("cp").value = newcp;
// 提交表单
document.getElementById("split_page_form").submit();
}
</script> </center>
</body>
</html>
总结:
这个项目看起来很简单,但是囊括了很多知识,包括
1’ java的多线程处理,接口及方法实现;
2‘ 数据库的基础操作及代码连接与表操作;
3’ 网络爬虫进行数据的收集,包含两种方法(仅我会的,这个是难点):
(1)java代码实现(正则表达式或者Jsoup)
(2)Heritrix工具实现(抑或其他工具)
4‘ Hadoop的基本配置和HDFS于eclipse的配置(后续阐述);
5’ HDFS的文件存取、MapReduce方法的编写(这个是难点);
6‘ DAO设计模式的代码实现;
7’ Jsp/Servlet的基础知识,以及JSTL和EL的了解
该项目实现的功能包含:
1‘ 数据收集(网络爬虫);
2’ 数据保存(DAO,数据库及HDFS);
3‘ 数据分析/规约(MapReduce);
4’ 搜索引擎(jsp/servlet,jstl/el);
5‘ web呈现(web项目)
这是一个简单的大数据搜索引擎的实现,综合性较强,需要多多阅读学习。
此外,还有一些缺陷未能实现,会有些麻烦,一个在编码格式上,一个在搜索的关键字交叉搜索上。
Lucene搜索引擎+HDFS+MR完成垂直搜索的更多相关文章
- 传智播客课程——Lucene搜索引擎
Lucene不是一个现成的程序,类似文件搜索程序或web网络爬行器或是一个网站的搜索引擎.Lucene是一个软件库,一个开发工具包,而不是一个具有完整特征的搜索应用程序.它本身只关注文本的索引和搜索. ...
- 使用 Apache Lucene 和 Solr 4 实现下一代搜索和分析
使用 Apache Lucene 和 Solr 4 实现下一代搜索和分析 使用搜索引擎计数构建快速.高效和可扩展的数据驱动应用程序 Apache Lucene™ 和 Solr™ 是强大的开源搜索技术, ...
- MATLAB垂直搜索图片中的白段
function [ top, bottom, middle, len ] = classify_by_vertical_white_belt( img ) % 垂直搜索图片中的白段, 记录具体信息. ...
- 用Lucene对文档进行索引搜索
问题 现在给出很多份文档,现在对某个搜索词感兴趣,想找到相关的文档. 简单搜索 一种简单粗暴的做法是: 1.读取每个文档:2.找到其中含有搜索词的文档:3.对找到的文档中搜索词出现的次数统计:4.根据 ...
- Nutch+Lucene搜索引擎开发实践
网络拓扑 图 1 网络拓扑图 安装Java JDK 首先查看系统是否已经安装了其它版本号的JDK,假设有,先要把其它版本号的JDK卸载. 用root用户登录系统. # rpm-qa|grep gcj ...
- 学习笔记CB011:lucene搜索引擎库、IKAnalyzer中文切词工具、检索服务、查询索引、导流、word2vec
影视剧字幕聊天语料库特点,把影视剧说话内容一句一句以回车换行罗列三千多万条中国话,相邻第二句很可能是第一句最好回答.一个问句有很多种回答,可以根据相关程度以及历史聊天记录所有回答排序,找到最优,是一个 ...
- 踏得网互联网新技术垂直搜索服务和分享 - HTML5动效/特效/动画搜索
版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/iefreer/article/details/34917729 当前主流搜索引擎在解决互联网技术创意 ...
- Lucene全文检索_分词_复杂搜索_中文分词器
1 Lucene简介 Lucene是apache下的一个开源的全文检索引擎工具包. 1.1 全文检索(Full-text Search) 1.1.1 定义 全文检索就是先分词创建索引,再执行搜索的过 ...
- Lucene搜索引擎入门
一.什么是全文检索? 就是在检索数据,数据的分类: 在计算机当中,比如说存在磁盘的文本文档,HTML页面,Word文档等等...... ...
随机推荐
- HTML Element 与 Node 的区别
Element 与 Node 的区别 <html> <head><title>Element & Node</title></head&g ...
- 简单总结一下 XSS
你听说过XSS吗? XSS(Cross-site scripting, 跨站脚本)是一种网站应用程序的安全漏洞攻击,是代码注入的一种. 研究表明,最近几年 XSS 已经超过 "缓冲区溢出&q ...
- MySQL--当mysqldump --single-transaction遇到alter table(2)
在上篇<MySQL--当mysqldump --single-transaction遇到alter table>中测试发现,在MySQL 5.6版本中,如果在mysqldump期间修改表, ...
- H3CNE实验:配置VLAN和VLAN端口
配置准备数据: | 设备名称 | IP地址 | VLAN网关 | 接口 | VLAN | |---------------|--------------|----------------|------ ...
- php7.0版本不再以类名命名构造函数
<?php class Car { var $color = "add"; function Car($color="green") { $this-&g ...
- php 时间问题
获得简单的日期 date() 函数的格式参数是必需的,它们规定如何格式化日期或时间. 下面列出了一些常用于日期的字符: d - 表示月里的某天(01-31) m - 表示月(01-12) Y - 表示 ...
- 关于Win7 内存变小处理方法
windows + R 输入msconfig 点击引导 点击高级选项 点击最大内存打钩,就好了,你重启,你的内存将恢复成原来的.
- 2-用EasyNetQ连接RabbitMQ(黄亮翻译)
如果你连接过关系数据库,例如SQL Server.你会发现EasyNetQ处理connections有点奇怪.和关系数据库通讯一直都是通过client开始的.Client 打开一个连接, 发出一个SQ ...
- jfinal框架新手使用之路及开发心得
从接触jfinal这个框架到现在差不多也有一个的时间了,因为之前接触的都是像spring ,springMVC,mybatis,struts2,hibernate这种传统,大多数公司都在用的这种框架. ...
- LoadRunner接口测试Error -27225报错解决
今天依照规范写了一个接口测试脚本,再执行的时候报Error -27225,核对了接口字段和字段值没发现错误,百度搜Error -27225错误没有相关解释.这个问题经过溯源找到了问题的所在,为了互帮互 ...