最近,我被分配了一个任务,以创建一个/ search服务,该服务使用来自搜索引擎的数据。 在调用搜索引擎之前和之后,它还会执行许多其他任务。 其中一些任务包括获取给定过滤器查询的配置规则,查询更正,意图识别,缓存查找以及从搜索引擎获取结果后对结果进行重新排名。
在这里,我将讨论可用于设计查询管道的一些模式,并讨论用于服务的函数(功能组合)的细节。
设计模式选择1.责任链(CoR)
通过这种方法,您将链接处理器,如果尚未处理/完成请求,则每个处理器将调用链中的下一个处理器。 每个处理器都拥有链中下一个处理器的实例。
CoR的目的是让处理器仅在先前的处理器无法解决请求时才处理该请求。 但是,我们的意图是运行所有处理器。
我们不选择CoR的原因:
· 我个人不喜欢处理器保留另一个(下一个)处理器实例的想法。 这将使单元测试变得复杂,并且我们将需要一个模拟框架来模拟这些下一个处理器对象。
· 链中的所有处理器都需要实现相同的接口,并且您不能链接不兼容的接口。
但是,如果您想从中间步骤返回,则此模式可能是您的理想选择。 但是,通过一些调整(不是干净的方法),也可以使用其他模式来实现相同的目的。
2.流程清单
另一种模式是维护处理器实例的列表。 然后依次调用每个处理器以处理请求。
该实现将类似于:
class QueryPipeline{
private Collection requestHandlers;
public void registerHandler(RequestHandler rh){
requestHandlers.add(rh)
}
public PipelineContextObject run(PipelineContextObject input){
for(RequestHandler rh: requesthandlers){
rh.handle(input);
}
return input;
}
}
PipelineContextObject可以具有管道流程中的输入和输出对象以及其他参数,才能满足请求。
在您的服务类中,您可以注入此管道对象并调用run方法。 可以在应用程序启动期间在QueryPipeline中注册RequestHandler对象。
这种模式很简单,易于实现。 但是,它有一些限制:
· 没有干净的解决方案可以从中间步骤返回结果-需要维护每个步骤之间的状态,并检查循环中的该状态以打破中断状态。
· 所有步骤都需要实现相同的接口。
3.功能组合
函数组合的思想是通过将一个函数作为参数传递给另一个函数来链接不同的函数。 许多现代语言已经支持这一点。 在Java 8中,可以使用"功能接口"来实现。
功能接口表示功能而不是数据,并且可以传递给另一个功能。
我们如何使用功能性接口创建管道?
创建一个自定义接口(这将是我们的Functional接口),管道中的所有步骤都将实现此接口。
实现此接口的每个类都将是它正在解决的功能,而这些将是我们的功能,这些功能将被链接在一起。
想法是按顺序调用这些函数,其中一个函数的输出将用作下一个函数的输入。
1.创建一个功能接口。
@FunctionalInterface
interface RequestHandler<I, O>{
O handle(I input);
/**
Returns a composed function that first applies this function to its input,
and then applies the function to the result.
*/
default RequestHandler<I, V> thenCall(
RequestHandler<? super O, ? extends V> nextHandler) {
Objects.requireNonNull(nextHandler);
return (I t) -> nextHandler.handle(RequestHandler.this.handle(t));
}
}
默认方法负责函数的组成。 以下是默认方法的扩展形式-请遵循以下注释以了解:
/**
when this method is called with the instance of next handler,
it creates a new RequestHandler object to call the next handlers handle method.
For next handler the input will be the output of current/this handler.
*/
default RequestHandler<I, V> thenCall(
RequestHandler<? super O, ? extends V> nextHandler) {
Objects.requireNonNull(nextHandler);
return new RequestHandler<I, V>() {
@Override
public V handle(I t) {
// First RequestHandler.this.handle(t) line is called
// output of RequestHandler.this.handle(t) is used as input in nextHandler.handle()
return nextHandler.handle(RequestHandler.this.handle(t));
}
};
}
/*
For instance:
say we have chain: queryCorrectionHandler.thenCall(searchEnginehandler)
In this case we are calling `.thenCall` on queryCorrectionHandler object.
This will return the new RequestHandler object in which handle method will
invoke searchEnginehandler.handle(queryCorrectionHandler.handle())
So, eventually queryCorrectionHandler.handle() is called first and outout of
queryCorrectionHandler.handle() is used as input of searchEnginehandler.handle() and so on
*/
2.实施此界面以创建您的具体功能。
@Service
class QueryUnderstandingHandler implements Requesthandler<SearchCriteria, SearchCriteria>{
@Override
public SearchCriteria handle(SearchCriteria sc){
//update sc based on QUS API result
return sc;
}
}
@Service
class SearchEngineRequestHandler implements Requesthandler<SearchCriteria, SearchResponse>{
@Override
public SearchResponse handle(SearchCriteria sc){
//get the data from search engine
return searchResponse;
}
}
@Service
class ReRankHandler implements Requesthandler<SearchResponse, SearchResponse>{
@Override
public SearchResponse handle(SearchResponse sr){
//re-rank the results based on some ML model
return sr;
}
}
3.然后,您可以在应用程序启动时构造对象并创建功能链。 如果您正在使用spring框架,则可以在Configuration类中创建一个bean。
下面是一个Spring Bean示例:
@Bean("queryPipeline")
public RequestHandler<SearchCriteria, SearchResponse> queryPipeline(
RequestHandler<SearchCriteria, SearchCriteria> queryUnderstandinghandler,
RequestHandler<SearchCriteria, SearchResponse> searchEnineRequestHandler,
RequestHandler<SearchResponse, SearchResponse> reRankHandler) {
return queryUnderstandinghandler.thenCall(searchEnineRequestHandler)
.thenCall(reRankHandler);
}
如果您没有使用spring框架,则可以调用以下方法,并在应用程序启动时将管道对象注入服务类中:
public static RequestHandler<SearchCriteria, SearchResponse> getQueryPipeline() {
RequestHandler<SearchCriteria, SearchCriteria> queryUnderstandinghandler=new QueryUnderstandingHandler();
RequestHandler<SearchCriteria, SearchResponse> searchEnineRequestHandler=new SearchEngineRequesthandler();
RequestHandler<SearchResponse, SearchResponse> reRankHandler=new ReRankHandler();
return queryUnderstandinghandler.thenCall(searchEnineRequestHandler)
.thenCall(reRankHandler);
}
queryPipeline Bean链接所有Bean /对象。 在这里,一个功能的响应用作下一个功能的请求。
4.最后,在服务类中使用queryPipeline bean:
class SearchServiceImpl implements SearchService{
// Autowire the bean or do constructor injection
private RequestHandler<SearchCriteria, SearchResponse> queryPipeline
@Override
public SearchResponse search(SearchCriteria sc){
return queryPipeline.handle(sc);
}
}我们如何链接功能接口的功能不一样?
假设我们的ReRank处理程序实现ReRankStrategy而不是RequestHandler接口。
// Re-ranking interface
interface ReRankStrategy{
SearchResponse reRank(SearchResponse sr);
}
// class implementing re-rank interface
class PopularityBasedReRank implements ReRankStrategy{
public SearchResponse reRank(SearchResponse sr){
// Do re-ranking of the results here and set back to sr
return sr;
}
}
解决方案很简单。 将ReRankStrategy实现包装在RequestHandler对象内。 然后在RequestHandler对象的handle()方法内执行reRankStrategy.reRank()方法。
包装reRankStrategy的其他方法是:
· 创建一个匿名类或
· 使用Lambda函数或
· 如果您的ReRankStrategy类是一个功能接口,则使用方法参考(仅一个非默认方法)
//Anonymous class
RequestHandler<SearchResponse, SearchResponse> reRankHandler=new RequestHandler<SearchResponse, SearchResponse>() {
@Override
public SearchResponse handle(SearchResponse input) {
return reRankStrategy.reRank(input);
}
};
// Lambda Function
RequestHandler<SearchResponse, SearchResponse> reRankHandler=input -> reRankStrategy.reRank(input);
// Method Reference
RequestHandler<SearchResponse, SearchResponse> reRankHandler=reRankStrategy::reRank;
因此,在这种情况下,queryPipeline Bean定义将如下所示:
@Bean("queryPipeline")
public RequestHandler<SearchCriteria, SearchResponse> queryPipeline(
RequestHandler<SearchCriteria, SearchCriteria> queryUnderstandinghandler,
RequestHandler<SearchCriteria, SearchResponse> searchEnineRequestHandler,
ReRankStrategy popularityBasedReRank) {
//wrap re-rank strategy inside RequestHandler
RequestHandler<SearchResponse, SearchResponse> reRankHandler=popularityBasedReRank::reRank;
return queryUnderstandinghandler.thenCall(searchEnineRequestHandler)
.thenCall(reRankHandler);
}
您始终可以使用Java 8 Function接口,而不是创建自己的RequestHandler接口。 创建新接口的原因是为了维护命名约定。 最好使用相对的接口名称,而不要使用" Function" :)
总结
在这三种用于实现查询管道的模式中,如果打算从中间状态返回,那么CoR将是理想的选择。 但是,具有"进程列表"更易于实现且不太复杂。
在我们的服务中,我们实现了功能组合,因为它更加整洁,因为它不会将下一处理器的对象保持在链中,并且可以将不兼容的接口绑定在一起。