solr進階: 如何定製搜索服務,擴展搜索邏輯
前兩天發了篇"剖析solr實用性",主要黑了solr建索引讓我覺得不舒服,還黑了solr隻是個通用的丟入了servlet容器裏的簡單服務,事實證明我錯了。這篇博客我要糾正並回答那篇博客裏自己總結的問題:如何定製solr實現自己的搜索服務。
solr是一個可擴展的服務,我們可以添加自己的包和類,在solr已經實現的default處理邏輯之上,再添加我們自己的搜索邏輯。實現手段就是繼承solr的基礎類,重寫或者改寫新的Filter,Search Component以及Request Handler類,來取代solr默認的處理類或者與之並存。我也是讀了源碼,參考了默認類的實現過程,才找到了定製的方法,下麵一一說明。
Request Handler
solrconfig.xml裏,對/select設置的默認處理類是solr.SearchHandler
<requestHandler name="/select" > <lst name="defaults"> <str name="echoParams">explicit</str> <int name="rows">10</int> <str name="df">usergoods_mix</str> </lst> </requestHandler>源碼中,SearchHandler類在org.apache.solr.handler.component包下,繼承了RequestHandlerBase類,他最主要的邏輯在handleRequestBody函數中,
@Override public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception, ParseException, InstantiationException, IllegalAccessException {}SearchHandler還有一個主要的成員變量
protected List<SearchComponent> components = null;SearchHandler主要職責是借助solrconfig.xml配置文件裏設置的默認或者新增的requestHandler參數以及search component類,構建一個ResponseBuilder
ResponseBuilder rb = new ResponseBuilder(req, rsp, components);
根據配置把各個search component該做的事都分配好,search component是真正讀取處理SolrQueryRequest內的查詢參數,往SolrQueryResponse裏填寫數據的地方。下麵會再介紹search component的兩個prepare()和process()函數。
定製Request Handler
默認的request handler直接得到你http裏url帶來的傳參,然後就開始根據配置分發任務,讓各自的component去處理查詢請求了。定製request handler的好處是,在這個步驟之中,你可以再額外進行一些別的邏輯處理,比如你可以寫寫log,在得到SolrQueryRequest後,傳入前查看處理一下裏麵的query,對SolrQueryResponse裏得到的數據集再做些處理等等。定製方式是自己繼承StandardRequestHandler類(該類繼承了SearchHandler),
package myplugin.handler; import org.apache.solr.handler.StandardRequestHandler; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; public class MySearchHandler extends StandardRequestHandler { public void handleRequestBody(SolrQueryRequest request, SolrQueryResponse response) throws Exception { super.handleRequestBody(request, response); //TODO: whatever you want } }並在solrconfig.xml裏配置即可起效。
<requestHandler name="/test" > <lst name="defaults"> <str name="echoParams">explicit</str> <int name="rows">10</int> <str name="df">usergoods_mix</str> </lst> </requestHandler>別忘了把自己的jar包放到webapp/的WEB-INF的lib目錄下。solr也為我們定製了別的handler,比如DisMaxRequestHandler,LukeRequestHandler,MoreLikeThisHandler
和SpellCheckerRequestHandler等等。大家可以自己看看源碼,知道他們分別做什麼。都在org.apache.solr.handler內。
Search Component
search component的意義遠比handler重要的多。solr已經在solrconfig.xml給我們定製了幾個默認的component
<searchComponent name="query" /> <searchComponent name="facet" /> <searchComponent name="mlt" /> <searchComponent name="highlight" /> <searchComponent name="stats" /> <searchComponent name="debug" />一般處理我們查詢請求的一定避免不了第一個query component。閱讀源碼發現,所有這些類都繼承SearchComponent。所以我們定製的時候也要繼承SearchComponent。
拿QueryComponent舉例子說明search component的重要性,最重要的兩個函數是
public class QueryComponent extends SearchComponent { public static final String COMPONENT_NAME = "query"; public void prepare(ResponseBuilder rb) throws IOException{} public void process(ResponseBuilder rb) throws IOException {} // ... }下麵截取一段prepare裏的代碼說明QueryComponent是怎麼讀取SolrQueryRequest(res)裏的內容,並最後把結果寫進SolrQueryResponse(rsp)的。
public void process(ResponseBuilder rb) throws IOException { SolrQueryRequest req = rb.req; SolrQueryResponse rsp = rb.rsp; SolrParams params = req.getParams(); if (!params.getBool(COMPONENT_NAME, true)) { return; } SolrIndexSearcher searcher = req.getSearcher(); // ...
DocListAndSet res = new DocListAndSet(); res.docList = new DocSlice(0, docs, luceneIds, null, docs, 0); if (rb.isNeedDocSet()) { // TODO: create a cache for this! List<Query> queries = new ArrayList<Query>(); queries.add(rb.getQuery()); List<Query> filters = rb.getFilters(); if (filters != null) queries.addAll(filters); res.docSet = searcher.getDocSet(queries); } rb.setResults(res); rsp.add("response",rb.getResults().docList);
對同一個request handler,可以按順序配置多個search component,這些component會在handler類裏各自得到自己的任務,把SolrQueryRequest和SolrQueryResponse傳承下去,在這個過程中,我們可以加入自己的component,定製我們想要的搜索結果和搜索邏輯
定製search component
我簡單把實現代碼帖一下,主要還是通過繼承基礎類,最後配置到solrconfig.xml內。
package myplugin.component;
import java.io.IOException;
import org.apache.solr.handler.component.ResponseBuilder;
import org.apache.solr.handler.component.SearchComponent;
public class MySearchComponent extends SearchComponent {
String query = null;
@Override
public void prepare(ResponseBuilder rb) throws IOException {
query = rb.req.getParams().get("q", "");
System.out.println("prepare: " + query);
}
@Override
public void process(ResponseBuilder rb) throws IOException {
if (query != null) {
rb.rsp.add("mytest", "zbf"); // <str name="mytest">zbf</str>
//SimpleOrderedMap map = (SimpleOrderedMap) builder.rsp.getValues();
//DocList doclist = (DocList) map.get("response");
// System.out.println("process: " + map.get("response").toString());
// System.out.println("process: " + map.get("mytest").toString());
}
}
@Override
public String getDescription() {
return "MySearchComponent";
}
@Override
public String getSource() {
return "";
}
@Override
public String getSourceId() {
return "";
}
@Override
public String getVersion() {
return "0.1";
}
}
主要就是在prepare()裏獲取到SolrQueryRequest裏的查詢query,在process()裏自己處理,並且獲取到前一次component處理得到的SolrQueryResponse,裏麵可能會有已經排好序的查詢數據集,你也可以做一些二次處理,簡單過濾,重新排序等事情
<requestHandler name="/test" > <lst name="defaults"> <str name="echoParams">explicit</str> <int name="rows">10</int> <str name="df">usergoods_mix</str> </lst> <arr name="components"> <str>query</str> <str>myComponent</str> </arr> </requestHandler> <searchComponent name="myComponent" > </searchComponent>
先聲明自己的searchComponent,然後放入handler裏使用起來,注意配置順序,因為component是按順序串接起來的。
定製Filter
最後簡單說下Filter,他的作用就是如果你自己定製了Filter,你就可以按自己的方式處理字符串。比如你的查詢query裏傳來的是“field:如何 定製 搜索服務”,如果你直接交給solr的api去做那麼"如何"使用的是field字段,但是空格之後的word都會歸結到default field裏,這是solr包裝了lucene的接口之後的結果,可能剛開始在組裝自己的查詢url的時候會不太適應,所以如果你定製一個自己的filter,就可以解決這樣的問題。
定製Filter要繼承兩個類。其實在配置IKAnalyzer的時候大家可以看到
<fieldType name="text_ik" > <analyzer type="index"> <tokenizer isMaxWordLength="true"/> <filter ignoreCase="true" words="stopwords.txt" enablePositionIncrements="true" /> <filter /> </analyzer> <analyzer type="query"> <!-- 同上 --> </analyzer> </fieldType>在tokenizer處使用一個類,在filter處使用一個Factory類。下麵我舉solr 3.1 cookbook上的例子說明一下。先是自己的filter類,
package pl.solr; import java.io.IOException; import org.apache.lucene.analysis.TokenFilter; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.tokenattributes.TermAttribute; public class ExampleFilter extends TokenFilter { private final TermAttribute termAttr = (TermAttribute) addAttribute(TermAttribute.class); public ExampleFilter(TokenStream stream) { super(stream); } @Override public boolean incrementToken() throws IOException { if (input.incrementToken()) { String term = termAttr.term(); if (term.length() <= 1) { return true; } StringBuffer buffer = new StringBuffer(); buffer.append(term.charAt(1)).append(term.charAt(0)). append(term.substring(2)); termAttr.setTermBuffer(buffer.toString()); termAttr.setTermLength(buffer.length()); return true; } return false; } }然後是工廠類,
package pl.solr; import org.apache.lucene.analysis.TokenStream; import org.apache.solr.analysis.BaseTokenFilterFactory; public class ExampleFilterFactory extends BaseTokenFilterFactory { @Override public TokenStream create(TokenStream stream) { return new ExampleFilter(stream); } }最後是配置文件:
<fieldtype name="exampleType" stored="true" indexed="true" > <analyzer> <tokenizer /> <filter /> </analyzer> </fieldtype>
總結
前幾天我也不知道solr的這幾個玩法,把solr給小看了。其實solr是一個高可擴展的東西,你要實現自己的搜索服務,就繼承solr的基本類,增加自己的擴展類到solr的配置文件裏,可以取代solr的默認處理類,也可以和solr的類共同處理。所以以上說的request handler,search component以及filter給我們實現了很好的擴展方式,用起來比較像SOA的架構,像OSGi裏的各部分組件。
之後如果對solr有更深的理解或者解讀,會再做分享。
(全文完)
最後更新:2017-04-04 07:03:34