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