Zgodnie z wcześniejszymi zapowiedziami pokaże wam dziś jak stworzyć podpowiadanie wyszukiwanych fraz podobnie jak działa to w wyszukiwarce Google. Wykorzystam do tego mechanizmy Facety, które są elementem Apache Solr’a i z których można z powodzeniem korzystać w Spring Boot’cie.
Dla wszystkich, którzy dalej mają wątpliwości krótki GIF jak działa autocomplete:
W tym wpisie będę korzystał z już wcześniej zaimportowanych danych w Apache Solr. Dlatego też odsyłam do poprzednich wpisów jeśli nie masz jeszcze danych na serwerze:
- http://codecouple.pl/2016/12/09/apache-solr-wyszukiwanie-pelnotekstowe/ - instalacja oraz wstęp,
- http://codecouple.pl/2016/12/30/apache-solr-dih-database-import-handler-mysql/ - import danych z MySQL(nieobowiązkowe).
Jeśli wykonaliśmy kroki z poprzednich wpisów powinniśmy mieć wypełnioną bazę danymi. Nie zaczniemy pracy bez odpowiednich zależności:
- spring-boot-starter-data-solr - pakiet z funkcjonalnością Solr’a,
- spring-boot-starter-thymeleaf - dla UI (opcjonalnie),
- lombok - niewymagany, ja korzystam, aby uniknąć zbędnego boilerplatu (opcjonalnie),
- boostrap - dla UI (opcjonalnie),
- spring-boot-starter-web - potrzebny do wystawiania endpointów,
- spring-boot-starter-test - dla testów (opcjonalnie)
Kolejnym krokiem do wykonania jest zamodelowanie naszego obiektu:
@Data
@NoArgsConstructor
@AllArgsConstructor
@SolrDocument(solrCoreName = “products”)
public class Product {
@Id
@Field
private String id;
@Field(value = "product\_name")
private String productName;
public Product(String productName) {
this.productName = productName;
}
}
Adnotacje @Data, @NoArgsConstructor, @AllArgsConstructor są to elementy Lomboka, który generuje za nas między innymi gettery i settery, konstruktory oraz inne podstawowe elementy klasy. Dzięki temu nie mamy zbędnego boilerplatu. Kolejną adnotacją jest @SolrDocument, jest to już adnotacja z pakietu spring-boot-starter-data-solr. Defniujemy w niej solrCoreName co oznacza nazwę Core, w którym przechowujemy nasze dane. Nazwa ta została ustawiona podczas tworzenia nowego Core (tworzenie Core opisywałem w poprzednim wpisie). @Id oznacza, iż to pole w klasie będzie unikalnym identyfikatorem dla instancji tej klasy.
@Field jest kolejną adnotacją z pakietu Solr’a. Wszystkie pola oznaczone tą adnotacją zostaną zapisane w bazie. Nazwa w bazie będzie taka sama jak nazwa pola, możemy to zmienić poprzez użycie @Field(value = "my_name").
Możemy także ustawić czy chcemy, aby nasze pole nie było persystowane @Indexed(readonly = true) lub czy chcemy, aby nasze pole było polem dynamicznym @Dynamic.
Gdy zamodelowaliśmy nasz obiekt domenowy należy go teraz w jakiś sposób zapisać w bazie. Użyjemy do tego abstrakcji dostarczanej przez Spring Boot’a, wykorzystujemy interfejs repozytorium. Stwórzmy nasz interfejs, który będzie rozszerzał SolrCrudRepository<T, ID>.
@Repository
public interface ProductRepository extends SolrCrudRepository<Product, String> {
@Facet(fields = { "product\_name" })
FacetPage<Product> findByProductNameIgnoreCaseStartingWith(String productName, Pageable pageable);
}
Nasze zapytanie tworzymy korzystając z abstrakcji Spring Data. Adnotacja @Facet służy nam do wywołania opcji Facet na naszym Core. Możemy także dodać adnotacje @Query i sami zdefiniować zapytanie. Ja natomiast korzystam z dobrodziejstw Spring Data. Teraz stwórzmy sobie serwis, który będzie warstwą komunikacji z usługami. Stwórzmy interfejs ProductService, a potem jego implementację ProductServiceImpl.
public interface ProductService {
FacetPage<Product> autocomplete(String query, Pageable pageable);
void addProduct(String productName);
}
Implementacja serwisu:
@Service
public class ProductServiceImpl implements ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductServiceImpl(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public FacetPage<Product> autocomplete(String query, Pageable pageable) {
if(StringUtils.isBlank(query)) {
return new SolrResultPage<>(Collections.emptyList());
}
return productRepository.findByProductNameIgnoreCaseStartingWith(query, pageable);
}
@Override
public void addProduct(String productName) {
productRepository.save(new Product(productName));
}
}
Mamy model, mamy repozytorium i serwis, pora na kontroler:
@Slf4j
@Controller
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}
@RequestMapping(value = "/autocomplete", produces = "application/json")
public @ResponseBody
Set<String> autoComplete(@RequestParam("term") String query,
@PageableDefault(page = 0, size = 1) Pageable pageable) {
if (!StringUtils.hasText(query)) {
return Collections.emptySet();
}
FacetPage<Product> result = productService.autocomplete(query, pageable);
Set<String> titles = new LinkedHashSet<>();
for (Page<FacetFieldEntry> page : result.getFacetResultPages()) {
for (FacetFieldEntry entry : page) {
Optional<String> entryValue = Optional.ofNullable(entry.getValue());
if(entryValue.isPresent() && entryValue.get().contains(query.toLowerCase())){
titles.add(StringUtils.capitalize(entryValue.get()));
}
}
}
return titles;
}
@GetMapping("/")
public String showSearchPage(){
return "index";
}
}
Teraz wystarczy dodać w klasie uruchamiającej przykładowe dane (jeśli nie robiliśmy tego wcześniej):
@SpringBootApplication
public class SolrAutocomplete implements CommandLineRunner{
@Autowired
private ProductService productService;
public static void main(String\[\] args) {
SpringApplication.run(SolrAutocomplete.class, args);
}
@Override
public void run(String... strings) throws Exception {
productService.addProduct("Code");
productService.addProduct("Couple");
productService.addProduct(".pl");
}
}
Ostatni punkt to dodanie widoku. Tworzymy plik index.html (jeśli korzystamy z Thymelaf’a dodajemy go do resources/templates). I wstawiamy nasz input do wpisywania:
…
…
W pliku ze skryptami dodajemy autocomplete (aby działała nam funkcja autocomplete musimy dodać jquery-iu, oraz jquery do sekcji head):
$(function() {
$(“#product_name”)
.autocomplete(
{
source : ‘http://localhost:8083/autocomplete‘,
minLength : 1,
});
});
Teraz, gdy udamy się pod nasz endpoint "/" pokaże się nam pole do wpisywania. Spróbujcie wpisać “C“, w podpowiedziach pokażą się słowa “Code“ i “Couple“. Całość znajdziecie na GitHub’ie.
W następnym wpisie kolejne funkcje Apache Solr’a!

