关于spring:在-Spring-Boot-中使用搜索引擎-Elasticsearch

40次阅读

共计 13576 个字符,预计需要花费 34 分钟才能阅读完成。


Elasticsearch 建设在 Apache Lucene 之上,于 2010 年由 Elasticsearch NV(现为 Elastic)首次公布。据 Elastic 网站称,它是一个 分布式开源搜寻和剖析引擎,实用于所有类型的数据,包含文本、数值、天文空间、结构化和非结构化。Elasticsearch 操作通过 REST API 实现。次要性能是:

  • 将文档存储在索引中,
  • 应用弱小的查问搜寻索引以获取这些文档,以及
  • 对数据运行剖析函数。

Spring Data Elasticsearch 提供了一个简略的接口来在 Elasticsearch 上执行这些操作,作为间接应用 REST API 的代替办法
在这里,咱们将应用 Spring Data Elasticsearch 来演示 Elasticsearch 的索引和搜寻性能,并在最初构建一个简略的搜寻应用程序,用于在产品库存中搜寻产品。

代码示例

本文附有 GitHub 上的工作代码示例。

Elasticsearch 概念

Elasticsearch 概念
理解 Elasticsearch 概念的最简略办法是用数据库进行类比,如下表所示:

Elasticsearch -> 数据库
索引 ->
文档 ->
文档 ->

咱们要搜寻或剖析的任何数据都作为文档存储在索引中。在 Spring Data 中,咱们以 POJO 的模式示意一个文档,并用注解对其进行润饰以定义到 Elasticsearch 文档的映射。

与数据库不同,存储在 Elasticsearch 中的文本首先由各种分析器解决。默认分析器通过罕用单词分隔符(如空格和标点符号)拆分文本,并删除罕用英语单词。

如果咱们存储文本“The sky is blue”,分析器会将其存储为蕴含“术语”“sky”和“blue”的文档。咱们将可能应用“blue sky”、“sky”或“blue”模式的文本搜寻此文档,并将匹配水平作为分数。

除了文本之外,Elasticsearch 还能够存储其余类型的数据,称为 Field Type(字段类型),如文档中 mapping-types(映射类型)局部所述。

启动 Elasticsearch 实例

在进一步探讨之前,让咱们启动一个 Elasticsearch 实例,咱们将应用它来运行咱们的示例。有多种运行 Elasticsearch 实例的办法:

  • 应用托管服务
  • 应用来自 AWS 或 Azure 等云提供商的托管服务
  • 通过在虚拟机集群中本人装置 Elasticsearch
  • 运行 Docker 镜像
    咱们将应用来自 Dockerhub 的 Docker 镜像,这对于咱们的演示应用程序来说曾经足够了。让咱们通过运行 Docker run 命令来启动 Elasticsearch 实例:
docker run -p 9200:9200 \
  -e "discovery.type=single-node" \
  docker.elastic.co/elasticsearch/elasticsearch:7.10.0

执行此命令将启动一个 Elasticsearch 实例,侦听端口 9200。咱们能够通过点击 URL http://localhost:9200 来验证实例状态,并在浏览器中查看后果输入:

{
  "name" : "8c06d897d156",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "Jkx..VyQ",
  "version" : {
  "number" : "7.10.0",
  ...
  },
  "tagline" : "You Know, for Search"
}

如果咱们的 Elasticsearch 实例启动胜利,应该看到下面的输入。

应用 REST API 进行索引和搜寻

Elasticsearch 操作通过 REST API 拜访。有两种办法能够将文档增加到索引中:

  • 一次增加一个文档,或者
  • 批量增加文档。

增加单个文档的 API 承受一个文档作为参数。

对 Elasticsearch 实例的简略 PUT 申请用于存储文档如下所示:

PUT /messages/_doc/1
{"message": "The Sky is blue today"}

这会将音讯 –“The Sky is blue today”存储为“messages”的索引中的文档。

咱们能够应用发送到搜寻 REST API 的搜寻查问来获取此文档:

GET /messages/search
{
  "query":
  {"match": {"message": "blue sky"}
  }
}

这里咱们发送一个 match 类型的查问来获取匹配字符串“blue sky”的文档。咱们能够通过多种形式指定用于搜寻文档的查问。Elasticsearch 提供了一个基于 JSON 的 查问 DSL(Domain Specific Language – 畛域特定语言)来定义查问。

对于批量增加,咱们须要提供一个蕴含相似以下代码段的条目标 JSON 文档:

POST /_bulk
{"index":{"_index":"productindex"}}{"_class":"..Product","name":"Corgi Toys .. Car",..."manufacturer":"Hornby"}{"index":{"_index":"productindex"}}{"_class":"..Product","name":"CLASSIC TOY .. BATTERY"...,"manufacturer":"ccf"}

应用 Spring Data 进行 Elasticsearch 操作

咱们有两种应用 Spring Data 拜访 Elasticsearch 的办法,如下所示:

  • Repositories:咱们在接口中定义方法,Elasticsearch 查问是在运行时依据办法名称生成的。
  • ElasticsearchRestTemplate:咱们应用办法链和原生查问创立查问,以便在绝对简单的场景中更好地管制创立 Elasticsearch 查问。

咱们将在以下各节中更具体地钻研这两种形式。

创立应用程序并增加依赖项

让咱们首先通过蕴含 web、thymeleaf 和 lombok 的依赖项,应用 Spring Initializr 创立咱们的应用程序。增加 thymeleaf 依赖项以便减少用户界面。

在 Maven pom.xml 中增加 spring-data-elasticsearch 依赖项:

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-elasticsearch</artifactId>
</dependency>

连贯到 Elasticsearch 实例

Spring Data Elasticsearch 应用 Java High Level REST Client (JHLC)”) 连贯到 Elasticsearch 服务器。JHLC 是 Elasticsearch 的默认客户端。咱们将创立一个 Spring Bean 配置来进行设置:

@Configuration
@EnableElasticsearch
Repositories(basePackages
        = "io.pratik.elasticsearch.repositories")@ComponentScan(basePackages = { "io.pratik.elasticsearch"})
public class ElasticsearchClientConfig extends
         AbstractElasticsearchConfiguration {
  @Override
  @Bean
  public RestHighLevelClient elasticsearchClient() {


  final ClientConfiguration clientConfiguration =
    ClientConfiguration
      .builder()
      .connectedTo("localhost:9200")
      .build();


  return RestClients.create(clientConfiguration).rest();}
}

在这里,咱们连贯到咱们之前启动的 Elasticsearch 实例。咱们能够通过增加更多属性(例如启用 ssl、设置超时等)来进一步自定义连贯。

为了调试和诊断,咱们将在 logback-spring.xml 的日志配置中关上传输级别的申请 / 响应日志:

public class Product {
  @Id
  private String id;
  
  @Field(type = FieldType.Text, name = "name")
  private String name;
  
  @Field(type = FieldType.Double, name = "price")
  private Double price;
  
  @Field(type = FieldType.Integer, name = "quantity")
  private Integer quantity;
  
  @Field(type = FieldType.Keyword, name = "category")
  private String category;
  
  @Field(type = FieldType.Text, name = "desc")
  private String description;
  
  @Field(type = FieldType.Keyword, name = "manufacturer")
  private String manufacturer;


  ...
}

表白文档

在咱们的示例中,咱们将按名称、品牌、价格或形容搜寻产品。因而,为了将产品作为文档存储在 Elasticsearch 中,咱们将产品示意为 POJO,并加上 Field 注解以配置 Elasticsearch 的映射,如下所示:

public class Product {
  @Id
  private String id;
  
  @Field(type = FieldType.Text, name = "name")
  private String name;
  
  @Field(type = FieldType.Double, name = "price")
  private Double price;
  
  @Field(type = FieldType.Integer, name = "quantity")
  private Integer quantity;
  
  @Field(type = FieldType.Keyword, name = "category")
  private String category;
  
  @Field(type = FieldType.Text, name = "desc")
  private String description;
  
  @Field(type = FieldType.Keyword, name = "manufacturer")
  private String manufacturer;


  ...
}

@Document 注解指定索引名称。

@Id 注解使注解字段成为文档的 _id,作为此索引中的惟一标识符。id 字段有 512 个字符的限度。

@Field 注解配置字段的类型。咱们还能够将名称设置为不同的字段名称。

在 Elasticsearch 中基于这些注解创立了名为 productindex 的索引。

应用 Spring Data Repository 进行索引和搜寻

存储库提供了应用 finder 办法拜访 Spring Data 中数据的最不便的办法。Elasticsearch 查问是依据办法名称创立的。然而,咱们必须小心防止产生低效的查问并给集群带来高负载。

让咱们通过扩大 ElasticsearchRepository 接口来创立一个 Spring Data 存储库接口:

public interface ProductRepository
    extends ElasticsearchRepository<Product, String> {}

此处 ProductRepository 类继承了 ElasticsearchRepository 接口中蕴含的 save()saveAll()find()findAll() 等办法。

索引

咱们当初将通过调用 save() 办法存储一个产品,调用 saveAll() 办法来批量索引,从而在索引中存储一些产品。在此之前,咱们将存储库接口放在一个服务类中:

@Service
public class ProductSearchServiceWithRepo {


  private ProductRepository productRepository;


  public void createProductIndexBulk(final List<Product> products) {productRepository.saveAll(products);
  }


  public void createProductIndex(final Product product) {productRepository.save(product);
  }
}

当咱们从 JUnit 调用这些办法时,咱们能够在跟踪日志中看到 REST API 调用索引和批量索引。

搜寻

为了满足咱们的搜寻要求,咱们将向存储库接口增加 finder 办法:

public interface ProductRepository
    extends ElasticsearchRepository<Product, String> {List<Product> findByName(String name);
  
  List<Product> findByNameContaining(String name);
  List<Product> findByManufacturerAndCategory
       (String manufacturer, String category);
}

在应用 JUnit 运行 findByName() 办法时,咱们能够看到在发送到服务器之前在跟踪日志中生成的 Elasticsearch 查问:

TRACE Sending request POST /productindex/_search? ..:
Request body: {.."query":{"bool":{"must":[{"query_string":{"query":"apple","fields":["name^1.0"],..}

相似地,通过运行
findByManufacturerAndCategory() 办法,咱们能够看到应用两个 query_string 参数对应两个字段——“manufacturer”和“category”生成的查问:

TRACE .. Sending request POST /productindex/_search..:
Request body: {.."query":{"bool":{"must":[{"query_string":{"query":"samsung","fields":["manufacturer^1.0"],..}},{"query_string":{"query":"laptop","fields":["category^1.0"],..}}],..}},"version":true}

有多种办法命名模式能够生成各种 Elasticsearch 查问。

应用 ElasticsearchRestTemplate 进行索引和搜寻

当咱们须要更多地管制咱们设计查问的形式,或者团队曾经把握了 Elasticsearch 语法时,Spring Data 存储库可能就不再适宜。

在这种状况下,咱们应用 ElasticsearchRestTemplate。它是 Elasticsearch 基于 HTTP 的新客户端,取代以前应用节点到节点二进制协定的 TransportClient。

ElasticsearchRestTemplate 实现了接口 ElasticsearchOperations,该接口负责底层搜寻和集群操的繁冗工作。

索引

该接口具备用于增加单个文档的办法 index() 和用于向索引增加多个文档的 bulkIndex() 办法。此处的代码片段显示了如何应用 bulkIndex() 将多个产品增加到索引“productindex”:

@Service
@Slf4j
public class ProductSearchService {


  private static final String PRODUCT_INDEX = "productindex";
  private ElasticsearchOperations elasticsearchOperations;


  public List<String> createProductIndexBulk
            (final List<Product> products) {List<IndexQuery> queries = products.stream()
      .map(product->
        new IndexQueryBuilder()
        .withId(product.getId().toString())
        .withObject(product).build())
      .collect(Collectors.toList());;
    
      return elasticsearchOperations
      .bulkIndex(queries,IndexCoordinates.of(PRODUCT_INDEX));
  }
  ...
}

要存储的文档蕴含在 IndexQuery 对象中。bulkIndex() 办法将 IndexQuery 对象列表和蕴含在 IndexCoordinates 中的 Index 名称作为输出。当咱们执行此办法时,咱们会取得批量申请的 REST API 跟踪:

Sending request POST /_bulk?timeout=1m with parameters:
Request body: {"index":{"_index":"productindex","_id":"383..35"}}{"_class":"..Product","id":"383..35","name":"New Apple..phone",..manufacturer":"apple"}
..
{"_class":"..Product","id":"d7a..34",.."manufacturer":"samsung"}

接下来,咱们应用 index() 办法增加单个文档:

@Service
@Slf4j
public class ProductSearchService {


  private static final String PRODUCT_INDEX = "productindex";
   
  private ElasticsearchOperations elasticsearchOperations;


  public String createProductIndex(Product product) {IndexQuery indexQuery = new IndexQueryBuilder()
         .withId(product.getId().toString())
         .withObject(product).build();


    String documentId = elasticsearchOperations
     .index(indexQuery, IndexCoordinates.of(PRODUCT_INDEX));


    return documentId;
  }
}

跟踪相应地显示了用于增加单个文档的 REST API PUT 申请。

Sending request PUT /productindex/_doc/59d..987..:
Request body: {"_class":"..Product","id":"59d..87",..,"manufacturer":"dell"}

搜寻

ElasticsearchRestTemplate 还具备 search() 办法,用于在索引中搜寻文档。此搜寻操作相似于 Elasticsearch 查问,是通过结构 Query 对象并将其传递给搜寻办法来构建的。

Query 对象具备三种变体 – NativeQueryyStringQueryCriteriaQuery,具体取决于咱们如何结构查问。让咱们构建一些用于搜寻产品的查问。

NativeQuery

NativeQuery 为应用示意 Elasticsearch 结构(如聚合、过滤和排序)的对象构建查问提供了最大的灵活性。这是用于搜寻与特定制造商匹配的产品的 NativeQuery

@Service
@Slf4j
public class ProductSearchService {


  private static final String PRODUCT_INDEX = "productindex";
  private ElasticsearchOperations elasticsearchOperations;


  public void findProductsByBrand(final String brandName) {


    QueryBuilder queryBuilder =
      QueryBuilders
      .matchQuery("manufacturer", brandName);


    Query searchQuery = new NativeSearchQueryBuilder()
      .withQuery(queryBuilder)
      .build();


    SearchHits<Product> productHits =
      elasticsearchOperations
      .search(searchQuery,
          Product.class,
          IndexCoordinates.of(PRODUCT_INDEX));
  }
}

在这里,咱们应用 NativeSearchQueryBuilder 构建查问,该查问应用 MatchQueryBuilder 指定蕴含字段“制造商”的匹配查问。

StringQuery

StringQuery 通过容许将原生 Elasticsearch 查问用作 JSON 字符串来提供齐全管制,如下所示:

@Service
@Slf4j
public class ProductSearchService {


  private static final String PRODUCT_INDEX = "productindex";
  private ElasticsearchOperations elasticsearchOperations;


  public void findByProductName(final String productName) {
    Query searchQuery = new StringQuery("{\"match\":{\"name\":{\"query\":\""+ productName + "\"}}}\"");
    
    SearchHits<Product> products = elasticsearchOperations.search(
      searchQuery,
      Product.class,
      IndexCoordinates.of(PRODUCT_INDEX_NAME));
  ...     
   }
}

在此代码片段中,咱们指定了一个简略的 match 查问,用于获取具备作为办法参数发送的特定名称的产品。

CriteriaQuery

应用 CriteriaQuery,咱们能够在不理解 Elasticsearch 任何术语的状况下构建查问。查问是应用带有 Criteria 对象的办法链构建的。每个对象指定一些用于搜寻文档的规范:

@Service
@Slf4j
public class ProductSearchService {


  private static final String PRODUCT_INDEX = "productindex";
   
  private ElasticsearchOperations elasticsearchOperations;


  public void findByProductPrice(final String productPrice) {Criteria criteria = new Criteria("price")
                  .greaterThan(10.0)
                  .lessThan(100.0);


    Query searchQuery = new CriteriaQuery(criteria);


    SearchHits<Product> products = elasticsearchOperations
       .search(searchQuery,
           Product.class,
           IndexCoordinates.of(PRODUCT_INDEX_NAME));
  }
}

在此代码片段中,咱们应用 CriteriaQuery 造成查问以获取价格大于 10.0 且小于 100.0 的产品。

构建搜寻应用程序

咱们当初将向咱们的应用程序增加一个用户界面,以查看产品搜寻的实际效果。用户界面将有一个搜寻输入框,用于按名称或形容搜寻产品。输入框将具备主动实现性能,以显示基于可用产品的倡议列表,如下所示:

咱们将为用户的搜寻输出创立主动实现倡议。而后依据与用户输出的搜寻文本亲密匹配的名称或形容搜寻产品。咱们将构建两个搜寻服务来实现这个用例:

  • 获取主动实现性能的搜寻倡议
  • 依据用户的搜寻查询处理搜寻产品的搜寻
    服务类 ProductSearchService 将蕴含搜寻和获取倡议的办法。

GitHub 存储库中提供了带有用户界面的成熟应用程序。

建设产品搜寻索引

productindex 与咱们之前用于运行 JUnit 测试的索引雷同。咱们将首先应用 Elasticsearch REST API 删除 productindex,以便在应用程序启动期间应用从咱们的 50 个时尚系列产品的示例数据集中加载的产品创立新的 productindex

curl -X DELETE http://localhost:9200/productindex

如果删除操作胜利,咱们将收到音讯 {"acknowledged": true}

当初,让咱们为库存中的产品创立一个索引。咱们将应用蕴含 50 种产品的示例数据集来构建咱们的索引。这些产品在 CSV 文件中被排列为独自的行。

每行都有三个属性 – id、name 和 description。咱们心愿在应用程序启动期间创立索引。请留神,在理论生产环境中,索引创立应该是一个独自的过程。咱们将读取 CSV 的每一行并将其增加到产品索引中:

@SpringBootApplication
@Slf4j
public class ProductsearchappApplication {
  ...
  @PostConstruct
  public void buildIndex() {esOps.indexOps(Product.class).refresh();
    productRepo.saveAll(prepareDataset());
  }


  private Collection<Product> prepareDataset() {Resource resource = new ClassPathResource("fashion-products.csv");
    ...
    return productList;
  }
}

在这个片段中,咱们通过从数据集中读取行并将这些行传递给存储库的 saveAll() 办法以将产品增加到索引中来进行一些预处理。在运行应用程序时,咱们能够在应用程序启动中看到以下跟踪日志。

...Sending request POST /_bulk?timeout=1m with parameters:
Request body: {"index":{"_index":"productindex"}}{"_class":"io.pratik.elasticsearch.productsearchapp.Product","name":"Hornby 2014 Catalogue","description":"Product Desc..talogue","manufacturer":"Hornby"}{"index":{"_index":"productindex"}}{"_class":"io.pratik.elasticsearch.productsearchapp.Product","name":"FunkyBuys..","description":"Size Name:Lar..& Smoke","manufacturer":"FunkyBuys"}{"index":{"_index":"productindex"}}.
...

应用多字段和含糊搜寻搜寻产品

上面是咱们在办法 processSearch() 中提交搜寻申请时如何解决搜寻申请:

@Service
@Slf4j
public class ProductSearchService {


  private static final String PRODUCT_INDEX = "productindex";


  private ElasticsearchOperations elasticsearchOperations;


  public List<Product> processSearch(final String query) {log.info("Search with query {}", query);
  
  // 1. Create query on multiple fields enabling fuzzy search
  QueryBuilder queryBuilder =
    QueryBuilders
    .multiMatchQuery(query, "name", "description")
    .fuzziness(Fuzziness.AUTO);


  Query searchQuery = new NativeSearchQueryBuilder()
            .withFilter(queryBuilder)
            .build();


  // 2. Execute search
  SearchHits<Product> productHits =
    elasticsearchOperations
    .search(searchQuery, Product.class,
    IndexCoordinates.of(PRODUCT_INDEX));


  // 3. Map searchHits to product list
  List<Product> productMatches = new ArrayList<Product>();
  productHits.forEach(searchHit->{productMatches.add(searchHit.getContent());
  });
  return productMatches;
  }...
}

在这里,咱们对多个字段执行搜寻 – 名称和形容。咱们还附加了 fuzziness() 来搜寻严密匹配的文本以解释拼写错误。

应用通配符搜寻获取倡议

接下来,咱们为搜寻文本框构建主动实现性能。当咱们在搜寻文本字段中输出内容时,咱们将通过应用搜寻框中输出的字符执行通配符搜寻来获取倡议。

咱们在 fetchSuggestions() 办法中构建此函数,如下所示:

@Service
@Slf4j
public class ProductSearchService {


  private static final String PRODUCT_INDEX = "productindex";


  public List<String> fetchSuggestions(String query) {
    QueryBuilder queryBuilder = QueryBuilders
      .wildcardQuery("name", query+"*");


    Query searchQuery = new NativeSearchQueryBuilder()
      .withFilter(queryBuilder)
      .withPageable(PageRequest.of(0, 5))
      .build();


    SearchHits<Product> searchSuggestions =
      elasticsearchOperations.search(searchQuery,
        Product.class,
      IndexCoordinates.of(PRODUCT_INDEX));
    
    List<String> suggestions = new ArrayList<String>();
    
    searchSuggestions.getSearchHits().forEach(searchHit->{suggestions.add(searchHit.getContent().getName());
    });
    return suggestions;
  }
}

咱们以搜寻输出文本的模式应用通配符查问,并附加 * 以便如果咱们输出“red”,咱们将取得以“red”结尾的倡议。咱们应用 withPageable() 办法将倡议的数量限度为 5。能够在此处看到正在运行的应用程序的搜寻后果的一些屏幕截图:

论断

在本文中,咱们介绍了 Elasticsearch 的次要操作——索引文档、批量索引和搜寻——它们以 REST API 的模式提供。Query DSL 与不同分析器的联合使搜寻变得十分弱小。

Spring Data Elasticsearch 通过应用 Spring Data Repositories 或 ElasticsearchRestTemplate 提供了不便的接口来拜访应用程序中的这些操作。

咱们最终构建了一个应用程序,在其中咱们看到了如何在靠近现实生活的应用程序中应用 Elasticsearch 的批量索引和搜寻性能。


  • 本文译自:Using Elasticsearch with Spring Boot – Reflectoring
  • 无关 ELK 套件请参考:ELK 教程 – 发现、剖析和可视化你的数据

正文完
 0