mirror of https://github.com/halo-dev/halo
Expose search service for plugin (#6239)
#### What type of PR is this? /kind feature /kind api-change /area core /area plugin /milestone 2.17.0 #### What this PR does / why we need it: This PR creates a SearchService and makes it invokable for plugins. #### Special notes for your reviewer: 1. Create a plugin 2. Publish all publication into Maven local repository by executing `./gradlew publishAllPublicationsToMavenLocalRepository` 3. Use `2.17.0-SNAPSHOT` as dependency version and refresh dependencies 4. Try to use the SearchService to search something. #### Does this PR introduce a user-facing change? ```release-note 为插件提供全文搜索服务 ```pull/6191/head^2
parent
2aaf64aa34
commit
4ea4bdf8b5
|
@ -0,0 +1,21 @@
|
||||||
|
package run.halo.app.search;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search service is used to search content.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
* @since 2.17.0
|
||||||
|
*/
|
||||||
|
public interface SearchService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform search.
|
||||||
|
*
|
||||||
|
* @param option search option must not be null
|
||||||
|
* @return search result
|
||||||
|
*/
|
||||||
|
Mono<SearchResult> search(SearchOption option);
|
||||||
|
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ import run.halo.app.plugin.event.HaloPluginStoppedEvent;
|
||||||
import run.halo.app.plugin.event.SpringPluginStartedEvent;
|
import run.halo.app.plugin.event.SpringPluginStartedEvent;
|
||||||
import run.halo.app.plugin.event.SpringPluginStoppedEvent;
|
import run.halo.app.plugin.event.SpringPluginStoppedEvent;
|
||||||
import run.halo.app.plugin.event.SpringPluginStoppingEvent;
|
import run.halo.app.plugin.event.SpringPluginStoppingEvent;
|
||||||
|
import run.halo.app.search.SearchService;
|
||||||
import run.halo.app.theme.DefaultTemplateNameResolver;
|
import run.halo.app.theme.DefaultTemplateNameResolver;
|
||||||
import run.halo.app.theme.ViewNameResolver;
|
import run.halo.app.theme.ViewNameResolver;
|
||||||
import run.halo.app.theme.finders.FinderRegistry;
|
import run.halo.app.theme.finders.FinderRegistry;
|
||||||
|
@ -136,6 +137,11 @@ public class DefaultPluginApplicationContextFactory implements PluginApplication
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
rootContext.getBeanProvider(SearchService.class)
|
||||||
|
.ifUnique(searchService ->
|
||||||
|
beanFactory.registerSingleton("searchService", searchService)
|
||||||
|
);
|
||||||
|
|
||||||
sw.stop();
|
sw.stop();
|
||||||
|
|
||||||
sw.start("LoadComponents");
|
sw.start("LoadComponents");
|
||||||
|
|
|
@ -43,7 +43,7 @@ public class PluginAutoConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PluginManager pluginManager(ApplicationContext context,
|
public SpringPluginManager pluginManager(ApplicationContext context,
|
||||||
SystemVersionSupplier systemVersionSupplier,
|
SystemVersionSupplier systemVersionSupplier,
|
||||||
PluginProperties pluginProperties) {
|
PluginProperties pluginProperties) {
|
||||||
return new HaloPluginManager(context, pluginProperties, systemVersionSupplier);
|
return new HaloPluginManager(context, pluginProperties, systemVersionSupplier);
|
||||||
|
|
|
@ -6,17 +6,13 @@ import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuil
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.validation.Validator;
|
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import org.springframework.web.server.ServerWebInputException;
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Schedulers;
|
|
||||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||||
import run.halo.app.extension.GroupVersion;
|
import run.halo.app.extension.GroupVersion;
|
||||||
import run.halo.app.infra.exception.RequestBodyValidationException;
|
|
||||||
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
|
||||||
import run.halo.app.search.post.PostHaloDocumentsProvider;
|
import run.halo.app.search.post.PostHaloDocumentsProvider;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
@ -24,13 +20,10 @@ public class IndexEndpoint implements CustomEndpoint {
|
||||||
|
|
||||||
private static final String API_VERSION = "api.halo.run/v1alpha1";
|
private static final String API_VERSION = "api.halo.run/v1alpha1";
|
||||||
|
|
||||||
private final ExtensionGetter extensionGetter;
|
private final SearchService searchService;
|
||||||
|
|
||||||
private final Validator validator;
|
public IndexEndpoint(SearchService searchService) {
|
||||||
|
this.searchService = searchService;
|
||||||
public IndexEndpoint(ExtensionGetter extensionGetter, Validator validator) {
|
|
||||||
this.extensionGetter = extensionGetter;
|
|
||||||
this.validator = validator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -92,17 +85,7 @@ public class IndexEndpoint implements CustomEndpoint {
|
||||||
option.setFilterExposed(true);
|
option.setFilterExposed(true);
|
||||||
option.setFilterPublished(true);
|
option.setFilterPublished(true);
|
||||||
option.setFilterRecycled(false);
|
option.setFilterRecycled(false);
|
||||||
// validate the option
|
return searchService.search(option);
|
||||||
var errors = validator.validateObject(option);
|
|
||||||
if (errors.hasErrors()) {
|
|
||||||
return Mono.error(new RequestBodyValidationException(errors));
|
|
||||||
}
|
|
||||||
return extensionGetter.getEnabledExtension(SearchEngine.class)
|
|
||||||
.filter(SearchEngine::available)
|
|
||||||
.switchIfEmpty(Mono.error(SearchEngineUnavailableException::new))
|
|
||||||
.flatMap(searchEngine -> Mono.fromSupplier(() ->
|
|
||||||
searchEngine.search(option)
|
|
||||||
).subscribeOn(Schedulers.boundedElastic()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
package run.halo.app.search;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.validation.Validator;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
import run.halo.app.infra.exception.RequestBodyValidationException;
|
||||||
|
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class SearchServiceImpl implements SearchService {
|
||||||
|
|
||||||
|
private final Validator validator;
|
||||||
|
|
||||||
|
private final ExtensionGetter extensionGetter;
|
||||||
|
|
||||||
|
public SearchServiceImpl(Validator validator, ExtensionGetter extensionGetter) {
|
||||||
|
this.validator = validator;
|
||||||
|
this.extensionGetter = extensionGetter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<SearchResult> search(SearchOption option) {
|
||||||
|
// validate the option
|
||||||
|
var errors = validator.validateObject(option);
|
||||||
|
if (errors.hasErrors()) {
|
||||||
|
return Mono.error(new RequestBodyValidationException(errors));
|
||||||
|
}
|
||||||
|
return extensionGetter.getEnabledExtension(SearchEngine.class)
|
||||||
|
.filter(SearchEngine::available)
|
||||||
|
.switchIfEmpty(Mono.error(SearchEngineUnavailableException::new))
|
||||||
|
.flatMap(searchEngine -> Mono.fromSupplier(() ->
|
||||||
|
searchEngine.search(option)
|
||||||
|
).subscribeOn(Schedulers.boundedElastic()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.pf4j.PluginWrapper;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||||
|
import run.halo.app.search.SearchService;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
class DefaultPluginApplicationContextFactoryTest {
|
||||||
|
|
||||||
|
@SpyBean
|
||||||
|
SpringPluginManager pluginManager;
|
||||||
|
|
||||||
|
DefaultPluginApplicationContextFactory factory;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
factory = new DefaultPluginApplicationContextFactory((SpringPluginManager) pluginManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateCorrectly() {
|
||||||
|
var pw = mock(PluginWrapper.class);
|
||||||
|
when(pw.getPluginClassLoader()).thenReturn(this.getClass().getClassLoader());
|
||||||
|
var sp = mock(SpringPlugin.class);
|
||||||
|
var pluginContext = new PluginContext.PluginContextBuilder()
|
||||||
|
.name("fake-plugin")
|
||||||
|
.version("1.0.0")
|
||||||
|
.build();
|
||||||
|
when(sp.getPluginContext()).thenReturn(pluginContext);
|
||||||
|
when(pw.getPlugin()).thenReturn(sp);
|
||||||
|
when(pluginManager.getPlugin("fake-plugin")).thenReturn(pw);
|
||||||
|
var context = factory.create("fake-plugin");
|
||||||
|
|
||||||
|
assertInstanceOf(PluginApplicationContext.class, context);
|
||||||
|
assertNotNull(context.getBeanProvider(SearchService.class).getIfUnique());
|
||||||
|
// TODO Add more assertions here.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -17,20 +17,16 @@ import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
import org.springframework.validation.Errors;
|
import org.springframework.validation.Errors;
|
||||||
import org.springframework.validation.Validator;
|
|
||||||
import org.springframework.web.reactive.function.server.HandlerStrategies;
|
import org.springframework.web.reactive.function.server.HandlerStrategies;
|
||||||
import org.springframework.web.server.handler.ResponseStatusExceptionHandler;
|
import org.springframework.web.server.handler.ResponseStatusExceptionHandler;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
import run.halo.app.infra.exception.RequestBodyValidationException;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class IndexEndpointTest {
|
class IndexEndpointTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
ExtensionGetter extensionGetter;
|
SearchService searchService;
|
||||||
|
|
||||||
@Mock
|
|
||||||
Validator validator;
|
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
IndexEndpoint endpoint;
|
IndexEndpoint endpoint;
|
||||||
|
@ -57,8 +53,8 @@ class IndexEndpointTest {
|
||||||
void shouldResponseBadRequestIfRequestBodyValidationFailed() {
|
void shouldResponseBadRequestIfRequestBodyValidationFailed() {
|
||||||
var option = new SearchOption();
|
var option = new SearchOption();
|
||||||
var errors = mock(Errors.class);
|
var errors = mock(Errors.class);
|
||||||
when(errors.hasErrors()).thenReturn(true);
|
when(searchService.search(any(SearchOption.class)))
|
||||||
when(validator.validateObject(any(SearchOption.class))).thenReturn(errors);
|
.thenReturn(Mono.error(new RequestBodyValidationException(errors)));
|
||||||
|
|
||||||
client.post().uri("/indices/-/search")
|
client.post().uri("/indices/-/search")
|
||||||
.bodyValue(option)
|
.bodyValue(option)
|
||||||
|
@ -70,17 +66,8 @@ class IndexEndpointTest {
|
||||||
void shouldSearchCorrectly() {
|
void shouldSearchCorrectly() {
|
||||||
var option = new SearchOption();
|
var option = new SearchOption();
|
||||||
option.setKeyword("halo");
|
option.setKeyword("halo");
|
||||||
|
|
||||||
var errors = mock(Errors.class);
|
|
||||||
when(errors.hasErrors()).thenReturn(false);
|
|
||||||
when(validator.validateObject(any(SearchOption.class))).thenReturn(errors);
|
|
||||||
|
|
||||||
var searchEngine = mock(SearchEngine.class);
|
|
||||||
when(searchEngine.available()).thenReturn(true);
|
|
||||||
var searchResult = new SearchResult();
|
var searchResult = new SearchResult();
|
||||||
when(searchEngine.search(any(SearchOption.class))).thenReturn(searchResult);
|
when(searchService.search(any(SearchOption.class))).thenReturn(Mono.just(searchResult));
|
||||||
when(extensionGetter.getEnabledExtension(SearchEngine.class))
|
|
||||||
.thenReturn(Mono.just(searchEngine));
|
|
||||||
|
|
||||||
client.post().uri("/indices/-/search")
|
client.post().uri("/indices/-/search")
|
||||||
.bodyValue(option)
|
.bodyValue(option)
|
||||||
|
@ -89,7 +76,7 @@ class IndexEndpointTest {
|
||||||
.expectBody(SearchResult.class)
|
.expectBody(SearchResult.class)
|
||||||
.isEqualTo(searchResult);
|
.isEqualTo(searchResult);
|
||||||
|
|
||||||
verify(searchEngine).search(assertArg(o -> {
|
verify(searchService).search(assertArg(o -> {
|
||||||
assertEquals("halo", o.getKeyword());
|
assertEquals("halo", o.getKeyword());
|
||||||
// make sure the filters are overwritten
|
// make sure the filters are overwritten
|
||||||
assertTrue(o.getFilterExposed());
|
assertTrue(o.getFilterExposed());
|
||||||
|
@ -101,15 +88,8 @@ class IndexEndpointTest {
|
||||||
@Test
|
@Test
|
||||||
void shouldBeCompatibleWithOldSearchApi() {
|
void shouldBeCompatibleWithOldSearchApi() {
|
||||||
var searchResult = new SearchResult();
|
var searchResult = new SearchResult();
|
||||||
var searchEngine = mock(SearchEngine.class);
|
when(searchService.search(any(SearchOption.class)))
|
||||||
when(searchEngine.available()).thenReturn(true);
|
.thenReturn(Mono.just(searchResult));
|
||||||
when(searchEngine.search(any(SearchOption.class))).thenReturn(searchResult);
|
|
||||||
when(extensionGetter.getEnabledExtension(SearchEngine.class))
|
|
||||||
.thenReturn(Mono.just(searchEngine));
|
|
||||||
|
|
||||||
var errors = mock(Errors.class);
|
|
||||||
when(errors.hasErrors()).thenReturn(false);
|
|
||||||
when(validator.validateObject(any(SearchOption.class))).thenReturn(errors);
|
|
||||||
|
|
||||||
client.get().uri(uriBuilder -> uriBuilder.path("/indices/post")
|
client.get().uri(uriBuilder -> uriBuilder.path("/indices/post")
|
||||||
.queryParam("keyword", "halo")
|
.queryParam("keyword", "halo")
|
||||||
|
@ -119,7 +99,7 @@ class IndexEndpointTest {
|
||||||
.expectBody(SearchResult.class)
|
.expectBody(SearchResult.class)
|
||||||
.isEqualTo(searchResult);
|
.isEqualTo(searchResult);
|
||||||
|
|
||||||
verify(searchEngine).search(assertArg(o -> {
|
verify(searchService).search(assertArg(o -> {
|
||||||
assertEquals("halo", o.getKeyword());
|
assertEquals("halo", o.getKeyword());
|
||||||
// make sure the filters are overwritten
|
// make sure the filters are overwritten
|
||||||
assertTrue(o.getFilterExposed());
|
assertTrue(o.getFilterExposed());
|
||||||
|
@ -130,14 +110,8 @@ class IndexEndpointTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFailWhenSearchEngineIsUnavailable() {
|
void shouldFailWhenSearchEngineIsUnavailable() {
|
||||||
var searchEngine = mock(SearchEngine.class);
|
when(searchService.search(any(SearchOption.class)))
|
||||||
when(searchEngine.available()).thenReturn(false);
|
.thenReturn(Mono.error(new SearchEngineUnavailableException()));
|
||||||
when(extensionGetter.getEnabledExtension(SearchEngine.class))
|
|
||||||
.thenReturn(Mono.just(searchEngine));
|
|
||||||
|
|
||||||
var errors = mock(Errors.class);
|
|
||||||
when(errors.hasErrors()).thenReturn(false);
|
|
||||||
when(validator.validateObject(any(SearchOption.class))).thenReturn(errors);
|
|
||||||
|
|
||||||
client.post().uri("/indices/-/search")
|
client.post().uri("/indices/-/search")
|
||||||
.bodyValue(new SearchOption())
|
.bodyValue(new SearchOption())
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
package run.halo.app.search;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.validation.Errors;
|
||||||
|
import org.springframework.validation.Validator;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
import run.halo.app.infra.exception.RequestBodyValidationException;
|
||||||
|
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class SearchServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
Validator validator;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ExtensionGetter extensionGetter;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
SearchServiceImpl searchService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowValidationErrorIfOptionIsInvalid() {
|
||||||
|
var option = new SearchOption();
|
||||||
|
option.setKeyword("halo");
|
||||||
|
|
||||||
|
var errors = mock(Errors.class);
|
||||||
|
when(errors.hasErrors()).thenReturn(true);
|
||||||
|
when(validator.validateObject(option)).thenReturn(errors);
|
||||||
|
|
||||||
|
searchService.search(option)
|
||||||
|
.as(StepVerifier::create)
|
||||||
|
.expectError(RequestBodyValidationException.class)
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowSearchEngineUnavailableExceptionIfNoSearchEngineFound() {
|
||||||
|
var option = new SearchOption();
|
||||||
|
option.setKeyword("halo");
|
||||||
|
|
||||||
|
var errors = mock(Errors.class);
|
||||||
|
when(errors.hasErrors()).thenReturn(false);
|
||||||
|
when(validator.validateObject(option)).thenReturn(errors);
|
||||||
|
|
||||||
|
when(extensionGetter.getEnabledExtension(SearchEngine.class)).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
searchService.search(option)
|
||||||
|
.as(StepVerifier::create)
|
||||||
|
.expectError(SearchEngineUnavailableException.class)
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowSearchEngineUnavailableExceptionIfNoSearchEngineAvailable() {
|
||||||
|
var option = new SearchOption();
|
||||||
|
option.setKeyword("halo");
|
||||||
|
|
||||||
|
var errors = mock(Errors.class);
|
||||||
|
when(errors.hasErrors()).thenReturn(false);
|
||||||
|
when(validator.validateObject(option)).thenReturn(errors);
|
||||||
|
|
||||||
|
when(extensionGetter.getEnabledExtension(SearchEngine.class))
|
||||||
|
.thenAnswer(invocation -> Mono.fromSupplier(() -> {
|
||||||
|
var searchEngine = mock(SearchEngine.class);
|
||||||
|
when(searchEngine.available()).thenReturn(false);
|
||||||
|
return searchEngine;
|
||||||
|
}));
|
||||||
|
|
||||||
|
searchService.search(option)
|
||||||
|
.as(StepVerifier::create)
|
||||||
|
.expectError(SearchEngineUnavailableException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSearch() {
|
||||||
|
var option = new SearchOption();
|
||||||
|
option.setKeyword("halo");
|
||||||
|
|
||||||
|
var errors = mock(Errors.class);
|
||||||
|
when(errors.hasErrors()).thenReturn(false);
|
||||||
|
when(validator.validateObject(option)).thenReturn(errors);
|
||||||
|
|
||||||
|
var searchResult = mock(SearchResult.class);
|
||||||
|
when(extensionGetter.getEnabledExtension(SearchEngine.class))
|
||||||
|
.thenAnswer(invocation -> Mono.fromSupplier(() -> {
|
||||||
|
var searchEngine = mock(SearchEngine.class);
|
||||||
|
when(searchEngine.available()).thenReturn(true);
|
||||||
|
when(searchEngine.search(option)).thenReturn(searchResult);
|
||||||
|
return searchEngine;
|
||||||
|
}));
|
||||||
|
|
||||||
|
searchService.search(option)
|
||||||
|
.as(StepVerifier::create)
|
||||||
|
.expectNext(searchResult)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue