最近产品提了一个新需求,希望做出像淘宝一样的搜索框(就是那种输入一个字,就有一个下拉框给你推荐以这个字开头的商品名称,然后随着你的输入,变化出不同的提示的那种关联搜索框)。至于效果图的话,嗯,我去扒一张淘宝的图贴上:
效果就类似这种,当然要想实现这样的效果,首先你得有个数据库,里边放着这些可以被检索到的名称用来备选。在页面与后端语言进行ajax交互的时候,将符合用户输入格式的数据传输到前台显示,大致就完成了。看起来思路是不是很简单。但是,有一个不可忽视的问题就是当你需要检索的数据表过于庞大的时候,前后台交互所需要的等待时间会变的比较长,影响用户的使用体验。那么怎么提升数据检索的效率,缩短检索时间,让用户很顺畅的使用搜索框搜索而感觉不到有数据交互等待的卡顿感。在处理这个问题的时候我选用的是之前已经使用过的lucene来建立关联数据的索引,根据传入的用户搜索词获取不同的匹配数据返回显示,用来缩短检索的时间。那么就会涉及到页面使用语言php与建立索引所使用的java之间的交互。下面介绍两种交互的方式供大家参考使用:
1.在php中使用exec()命令执行java jar 将参数以java命令行参数的形式传入java程序中执行,将输出存入某个特定的txt文件中,然后php程序从文件中将数据读取出来使用。php执行java命令代码如下:
if(!file_exists($file_path)){//如果不存在java输出的数据文件,则执行java命令生成数据文件
$command="java -jar /data/article.jar search ".$this->user_id." ".$time_start." ".$time." ".$file_path;
exec($command);
}
这样就实现了php与java的数据交互。但我觉得有一点点的费劲,非得读文件,这多占硬盘空间啊。就想着是不是还有其他更简单的方法,于是我就想到实现restful API让java提供一个http服务,让php访问java提供的http接口获取特定格式的数据。这就是接下来要说的第二种交互方式了
2.使用java的jersey框架实现restful API提供http服务,php程序通过http请求获取约定格式的数据。jersey框架使用代码如下(PS:我没有使用maven管理依赖包,所以在eclipse中我先从maven服务器上将jersey的示例代码下载下来然后转成java项目使用的):
首先是启动http服务监听特定端口请求的代码:
package jersey;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Properties;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;
/**
* Main class.
*
*/
public class Main {
// Base URI the Grizzly HTTP server will listen on
public static String BASE_URI = "http://localhost:8080/searchOp/";
/**
* Starts Grizzly HTTP server exposing JAX-RS resources defined in this application.
* @return Grizzly HTTP server.
*/
public static HttpServer startServer(String configFilePath) {
String ip="";
ip = readConfigInfo(configFilePath);
BASE_URI = "http://"+ip+":8080/searchOp/";
// create a resource config that scans for JAX-RS resources and providers
// in jersey package
final ResourceConfig rc = new ResourceConfig().packages("jersey");//将资源注册到服务上,用来接收用户的请求
// create and start a new instance of grizzly http server
// exposing the Jersey application at BASE_URI
return GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), rc);
}
public static HttpServer startServer(String ip, String port) {
BASE_URI = "http://"+ip+":"+port+"/searchOp/";
// create a resource config that scans for JAX-RS resources and providers
// in jersey package
final ResourceConfig rc = new ResourceConfig().packages("jersey");
// create and start a new instance of grizzly http server
// exposing the Jersey application at BASE_URI
return GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), rc);
}
public static void closeServer(HttpServer server){
server.shutdown();
}
public static String readConfigInfo(String configFilePath){
Properties properties = new Properties();
InputStream inputStream = null;
if(configFilePath.equals("")){
inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("config.properties");//读取数据库连接配置文件
}else{
try {
inputStream = new FileInputStream(configFilePath);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
String ip = "localhost";
try {
properties.load(inputStream);
ip = properties.getProperty("ip");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return ip;
}
/**
* Main method.
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
HttpServer server = null;
if(args.length > 0){
if(args.length == 1)
server = startServer(args[0]);
else
server = startServer(args[0],args[1]);
}else{
server = startServer("");
}
System.out.println(String.format("Jersey app started with WADL available at "
+ "%sapplication.wadl\nHit enter to stop it...", BASE_URI));
System.in.read();
closeServer(server);
}
}
下面是注册的根资源类代码(因为想要让里面的变量在服务运行周期里被所有用户共用,而不是每一个链接都生成一个新的对象,将根资源类声明为单例类即可满足要求。):
package jersey;
import java.util.List;
import javax.inject.Singleton;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import org.json.simple.JSONArray;
/**
* Root resource (exposed at "myresource" path)
*/
@Singleton//声明为单例类
@Path("myresource")
public class MyResource {
private Dealer d = null;
private final static String CHARSET_UTF_8 = "charset=utf-8";
public MyResource(){
d = new Dealer();
}
/**
* Method handling HTTP GET requests. The returned object will be sent
* to the client as "APPLICATION_JSON" media type.
*
* @return List that will be returned as a APPLICATION_JSON response.
*/
@GET
@Path("getrelative")
@Produces(MediaType.TEXT_PLAIN + ";" + CHARSET_UTF_8)
public String getRelativeReq(
@QueryParam("keyword") String keywordname,
@DefaultValue("1") @QueryParam("type") int type) {
List<String> result = d.getRelativeTitle(keywordname,type,5);
return JSONArray.toJSONString(result);
}
@GET
@Path("getsearch")
@Produces(MediaType.TEXT_PLAIN + ";" + CHARSET_UTF_8)
public String getSearchReq(
@QueryParam("keyword") String keywordname,
@DefaultValue("1") @QueryParam("type") int type) {
// Dealer d = new Dealer();
List<String> result = d.getRelativeId(keywordname, type, 1000000);
return JSONArray.toJSONString(result);
}
@GET
@Path("dealinfo")
@Produces(MediaType.TEXT_PLAIN + ";" + CHARSET_UTF_8)
public boolean dealInfoReq(@QueryParam("path") String pathname) {
// Dealer d = new Dealer();
boolean result = d.dealIndexInfo(pathname);
return result;
}
@GET
@Path("getUserList")
@Produces(MediaType.TEXT_PLAIN + ";" + CHARSET_UTF_8)
public String getUserList(@DefaultValue("1") @QueryParam("pageIndex") int pageIndex,@DefaultValue("20") @QueryParam("pageSize") int pageSize,@QueryParam("name") String name,@DefaultValue("-1") @QueryParam("type") int type,@QueryParam("business") String business,@DefaultValue("-1") @QueryParam("area_id") int area_id) {
// Dealer d = new Dealer();
LuceneResult result = d.getUserList(pageIndex,pageSize,name,type,business,area_id);
return String.format("{\"total\":%d,\"result\":%s}", result.total, JSONArray.toJSONString(result.result));
}
@GET
@Path("indexInfo")
@Produces(MediaType.TEXT_PLAIN + ";" + CHARSET_UTF_8)
public boolean indexInfo(@QueryParam("path") String path) {
// Dealer d = new Dealer();
boolean result = d.indexInfo(path);
return result;
}
@GET
@Produces(MediaType.TEXT_PLAIN + ";" + CHARSET_UTF_8)
public String sayHello(){
return "Welcome to use Search Optimization!";
}
}
后面就是通过Dealer类进行具体的操作,然后将符合要求的数据返回即可。因为这个关联搜索使用lucene作为数据索引,所以对于索引的更新频率一定要掌握好,而且要采用读写分离的方式更新索引,保证索引能够实时更新但不会影响数据的读取操作。在这里我采用的读写分离的方法是:索引更新时直接更新的索引存放目录下的索引文件,但是在读取索引中的数据时使用的是内存读取方式,通过内存存储要读取的索引数据,这样就算文件被更改也不会导致数据出错。在这里要用到的lucene的两种Directory:FSDirectory(文件模式)和RAMDirectory(内存模式),在这里给出这两种directory的使用场景代码:
package jersey;
import java.io.IOException;
import java.nio.file.Paths;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
/** Index all article.
* <p>生成索引
* This is a command-line application demonstrating simple Lucene indexing.
* Run it with no command-line arguments for usage information.
*/
public class IndexInfo {
private IndexWriter writer = null;
public IndexInfo(boolean create,String path) {
this.initWriter(create,path);
}
/** Index all text files under a directory. */
private void initWriter(boolean create,String path){
String indexPath = path;
Directory dir = null;
try {
dir = FSDirectory.open(Paths.get(indexPath));
Analyzer analyzer = new StandardAnalyzer();
IndexWriterConfig iwc = new IndexWriterConfig(analyzer);
if (create) {
iwc.setOpenMode(OpenMode.CREATE);
} else {
iwc.setOpenMode(OpenMode.CREATE_OR_APPEND);
}
iwc.setMaxBufferedDocs(1000);
this.writer = new IndexWriter(dir, iwc);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* type
* @param id
* @param type
* @param business
* @param name
* @return
*/
public boolean createUserIndex(String id, int type,String business, String name,String area_id){
//Date start = new Date();
try {
Document doc = new Document();
doc.add(new StringField("all", "1", Field.Store.YES));
doc.add(new StringField("id", id, Field.Store.YES));
doc.add(new StringField("type", Integer.toString(type), Field.Store.YES));
doc.add(new TextField("name", name.toLowerCase(), Field.Store.YES));
doc.add(new TextField("business", business, Field.Store.YES));
doc.add(new StringField("area_id", area_id, Field.Store.YES));
this.writer.addDocument(doc);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// writer.close();
//Date end = new Date();
//System.out.println(end.getTime() - start.getTime() + " total milliseconds");
return true;
}
public boolean createIndex(String id, String title, int type, int time) {
//Date start = new Date();
try {
Document doc = new Document();
Field pathField = new StringField("path", id, Field.Store.YES);
doc.add(pathField);
doc.add(new StringField("type", Integer.toString(type), Field.Store.YES));
doc.add(new StringField("time", Integer.toString(time), Field.Store.YES));
doc.add(new StringField("titleLen", Integer.toString(title.length()), Field.Store.YES));
doc.add(new TextField("title", title.toLowerCase(), Field.Store.YES));
doc.add(new StringField("titleOld", title, Field.Store.YES));
this.writer.addDocument(doc);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// writer.close();
//Date end = new Date();
//System.out.println(end.getTime() - start.getTime() + " total milliseconds");
return true;
}
public void closeWriter(){
try {
this.writer.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
注意:如果需要对索引中存储的某一字段进行模糊匹配(切词匹配)的话,该字段存储到索引文件中需用TextField而非StringField
package jersey;
import java.io.IOException;
import java.nio.file.Paths;
import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.RAMDirectory;
public class SearchKeyword {//搜索索引
private IndexSearcher searcher = null;
private Analyzer analyzer = null;
public SearchKeyword(String path) {
IndexReader reader = null;
try {
//reader = DirectoryReader.open(FSDirectory.open(Paths.get(index)));
FSDirectory fsDirectory = FSDirectory.open(Paths.get(path));
RAMDirectory ramDirectory = new RAMDirectory(fsDirectory, IOContext.READONCE);//使用内存读取数据
fsDirectory.close();
reader = DirectoryReader.open(ramDirectory);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.searcher = new IndexSearcher(reader);
this.analyzer = new StandardAnalyzer();
}
public SearchKeyword(Directory dir) {
try {
this.searcher = new IndexSearcher(DirectoryReader.open(dir));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.analyzer = new StandardAnalyzer();
}
public TopDocs findKeyword(String field, String keyword) {
try {
QueryParser parser = new QueryParser(field, analyzer);
Query query = parser.parse(keyword);
return searcher.search(query, 100000000);
} catch(Exception e){
e.printStackTrace();
}
return null;
}
public List<Element> getInfo(String keyword,int type){
return this.getInfo(keyword, type,-1, -1);
}
public List<Element> getInfoAll(int timestamp_start, int timestamp_end){
String timeQuery = "";
if(timestamp_start!=-1 && timestamp_end != -1){
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
timeQuery = String.format("time:[%s TO %s]", sdf.format(new Date(timestamp_start * 1000L)),sdf.format(new Date((timestamp_end - 60*60*12) * 1000L)));
}else
return null;
TopDocs tdocs = findKeyword("title",timeQuery);
List<Element> map = new ArrayList<Element>();
if(tdocs!=null){
for(ScoreDoc hit: tdocs.scoreDocs){
Element element = new Element();
Document doc = null;
try {
doc = searcher.doc(hit.doc);
} catch (IOException e) {
continue;
}
element.setId(doc.get("path"));
element.setTitleLen(Integer.valueOf(doc.get("titleLen")));
element.setType(Integer.valueOf(doc.get("type")));
element.setTime(Integer.valueOf(doc.get("time")));
element.setTitle(doc.get("title"));
map.add(element);
}
}
return map;
}
public List<Element> getInfo(String keyword,int type, int timestamp_start, int timestamp_end){
keyword = keyword.toLowerCase();
String timeQuery = "";
if(timestamp_start!=-1 && timestamp_end != -1){
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
timeQuery = String.format(" AND time:[%s TO %s]", sdf.format(new Date(timestamp_start * 1000L)),sdf.format(new Date((timestamp_end - 60*60*12) * 1000L)));
}
TopDocs tdocs = findKeyword("title","title:\""+keyword+"\"" + timeQuery+" AND type:\""+type+"\"");
List<Element> map = new ArrayList<Element>();
if(tdocs!=null){
for(ScoreDoc hit: tdocs.scoreDocs){
Element element = new Element();
Document doc = null;
try {
doc = searcher.doc(hit.doc);
} catch (IOException e) {
continue;
}
element.setId(doc.get("path"));
element.setTitleLen(Integer.valueOf(doc.get("titleLen")));
element.setType(Integer.valueOf(doc.get("type")));
element.setTime(Integer.valueOf(doc.get("time")));
element.setTitle(doc.get("titleOld"));
map.add(element);
}
}
return map;
}
public LuceneResult getUserInfo(int pageIndex,int pageSize,String name, int type, String business, int area_id){
StringBuilder sb = new StringBuilder();
sb.append("all:1");
if(name!=null &&name.length()>0){
sb.append(" AND name:\""+name+"\"");
}
if(type!=-1){
if(type==0){
sb.append(" AND (type:0 OR type:3)");
}else
sb.append(" AND type:"+type);
}
if(business!=null &&business.length()>0){
sb.append(" AND business:\""+business+"\"");
}
if(area_id!=-1){
sb.append(" AND area_id:"+area_id);
}
TopDocs tdocs = findKeyword("all",sb.toString());
List<Element> map = new ArrayList<Element>();
if(tdocs!=null){
for(int i = (pageIndex-1)*pageSize; i < pageIndex*pageSize; i++){
if(i==tdocs.scoreDocs.length)
break;
ScoreDoc hit=tdocs.scoreDocs[i];
Element element = new Element();
Document doc = null;
try {
doc = searcher.doc(hit.doc);
} catch (IOException e) {
continue;
}
element.setId(doc.get("id"));
element.setType(Integer.valueOf(doc.get("type")));
map.add(element);
}
}
LuceneResult result = new LuceneResult();
result.total = tdocs.scoreDocs.length;
result.result = map;
return result;
}
}
到此,java端的代码操作流程就完结了。现在,我们来看看php端如何请求和获取数据了(java代码打的jar包一定放在服务器上运行然后让php程序发送http请求(不要直接在页面上直接用ajax访问)啊,不然的话访问不了的哦~)
public function actionRelative(){
//搜索关联词语
if(!empty($_REQUEST["keyword"])){
$base_url = "http://".SEARCH_SERVER."/searchOp/myresource/getrelative?keyword=".urlencode($_REQUEST['keyword'])."&type=".$_REQUEST['type'];
$this->dies(CurlGet($base_url));//CurlGet()用来发送http请求的
}
}
function CurlGet($url){
//初始化
$ch=curl_init();
//设置选项,包括URL
curl_setopt($ch,CURLOPT_URL,$url);
// 设置header
curl_setopt($ch,CURLOPT_HEADER,0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// 设置cURL 参数,要求结果保存到字符串中还是输出到屏幕上
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
//执行并获取HTML文档内容
$output=curl_exec($ch);
//释放curl句柄
curl_close($ch);
//打印获得的数据
//print_r($output);
return $output;
}
到这里就完成了全部功能。使用http服务交互还是蛮方便的~mark一下,与君共勉。