blog

スムーズなショッピングモール(V): Elasticsearchでコモディティ検索を実現する

毎日コツコツ勉強\nショッピングモール:環境構築\nショッピングモール:分散ファイルシステムFastDFS\nショッピングモール:製品管理\nLua、OpenResty、Canal:広告キャッシュと同...

Jan 4, 2021 · 9 min. read
シェア

一生懸命勉強すれば、毎日上達します

事前準備

検索マイクロサービスのAPIプロジェクト構築

<dependencies>
 <!--goods API依存関係>
 <dependency>
 <groupId>com.robod</groupId>
 <artifactId>changgou-service-goods-api</artifactId>
 <version>1.0-SNAPSHOT</version>
 </dependency>
 <!--SpringDataES依存関係>
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
 </dependency>
</dependencies>

検索マイクロサービス構築

検索マイクロサービスとしてchanggou-service の下に新しい changgou-service-search プロジェクトを作成します。検索マイクロサービスの内部では、APIプロジェクトのJavaBeanとFeignインタフェースを使用する必要があるので、依存関係としてsearch-apiとgoods-apiを追加します。

<dependencies>
 <!--依存検索api>
 <dependency>
 <groupId>com.robod</groupId>
 <artifactId>changgou-service-search-api</artifactId>
 <version>1.0-SNAPSHOT</version>
 </dependency>
 <dependency>
 <groupId>com.robod</groupId>
 <artifactId>changgou-service-goods-api</artifactId>
 <version>1.0-SNAPSHOT</version>
 </dependency>
</dependencies>

スタートアップクラスとコンフィギュレーションファイルは必須。

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableEurekaClient
@EnableFeignClients(basePackages = "com.robod.goods.feign")
@EnableElasticsearchRepositories(basePackages = "com.robod.mapper")
public class SearchApplication {
 public static void main(String[] args) {
 //SpringBootのnettyとelasticsearchのnetty関連のjarの競合を解決する
 System.setProperty("es.set.netty.runtime.available.processors", "false");
 SpringApplication.run(SearchApplication.class,args);
 }
}
server:
 port: 18085
spring:
 application:
 name: search
 data:
 elasticsearch:
 cluster-name: my-application # クラスタノードの名前はesの設定ファイルで設定する。
 cluster-nodes: 192.0:9300 # ここでは、TCPポートを使用するので、9300である。
eureka:
 client:
 service-url:
 defaultZone: "http://...1:7100"/eureka
 instance:
 prefer-ip-address: true
feign:
 hystrix:
 enabled: true
#タイムアウトの設定
ribbon:
 ReadTimeout: 500000 # Feignデータの読み込み要求タイムアウト時間
hystrix:
 command:
 default:
 execution:
 isolation:
 thread:
 timeoutInMilliseconds: 50000 # feign接続タイムアウト

データインポート ES

MySQLからESへのデータインポートは、大まかに以下のステップに分けられます:

まずJavaBeanを作成し、関連するマッピング・コンフィギュレーション、インデックス、タイプ、フィールドを定義します:

@Data
@Document(indexName = "sku_info", type = "docs")
public class SkuInfo implements Serializable {
 @Id
 private Long id;//商品IDだけでなく商品番号も
 /**
 * SKU 
 * FieldType.Text単語の分割をサポートする
 * analyzer 単語分割のインデックスを作成する
 * searchAnalyzer 検索に使用するセグメンター
 */
 @Field(type = FieldType.Text, analyzer = "ik_smart",searchAnalyzer = "ik_smart")
 private String name;
 @Field(type = FieldType.Double)
 private Long price;//商品価格、単位:元
 private Integer num;//在庫数
 private String image;//商品イメージ
 private String status;//製品ステータス, 1-通常, 2-ダウンロード済み, 3-削除済み
 private LocalDateTime createTime;//作成時間
 private LocalDateTime updateTime;//更新時間
 private String isDefault; //デフォルトの
 private Long spuId;//SPU_ID
 private Long categoryId;//カテゴリID
 @Field(type = FieldType.Keyword)
 private String categoryName;//単語分割されていないカテゴリ名
 @Field(type = FieldType.Keyword)
 private String brandName;//ブランド名、サブ分類されていない
 private String spec;// 
 private Map<String, Object> specMap;//仕様パラメータ
}

SkuInfoで、Indexを "sku_info "に、Tpyeを "docs "に設定し、いくつかのフィールドに区切りを設定します。そして

@FeignClient(name = "goods")
@RequestMapping("/sku")
public interface SkuFeign {
 /**
 * 全ての SKU データを検索する
 * @return
 */
 @GetMapping
 Result<List<Sku>> findAll();
}

このFeignを使用して、GoodsマイクロサービスのfindAllメソッドを呼び出し、データベース内のすべてのSkuデータを取得します。最後に、データインポート機能を実装します。

//SkuEsController
@GetMapping("/import")
public Result importData(){
 skuEsService.importData();
 return new Result(true, StatusCode.OK,"データは正常にインポートされた");
}
-----------------------------------------------------------
//SkuEsServiceImpl
@Override
public void importData() {
 List<Sku> skuList = skuFeign.findAll().getData();
 List<SkuInfo> skuInfos = JSON.parseArray(JSON.toJSONString(skuList), SkuInfo.class);
 //マップに文字列を指定すると、マップのキーが自動的にフィールドを生成する
 for (SkuInfo skuInfo : skuInfos) {
 Map<String,Object> map = JSON.parseObject(skuInfo.getSpec(),Map.class);
 skuInfo.setSpecMap(map);
 }
 skuEsMapper.saveAll(skuInfos);
}
-------------------------------------------------------------
//ElasticsearchRepository から継承、総称型 SkuInfo、主キー型 Long
public interface SkuEsMapper extends ElasticsearchRepository<SkuInfo,Long> {
}

プログラムを起動し、"http://localhost:18085"/search/importにアクセスしてインポートを開始します。

長い待ち時間の後、90,000以上のデータがESにインポートされました。少し時間がかかり、15分ほどかかりましたが、これは仮想マシンの設定に関係していると思われます。

インデックスに問題があるようです。インデックスを削除し、プロジェクトを開始すれば問題ありません!

機能の実装

キーワード検索

この機能を実装する前に、フロントエンドとバックエンドでパラメータを渡す際のフォーマットを指定する必要があります。ビデオではMapを使っていますが、Mapは可読性が悪すぎて良くないと思います。そこで、search-apiプロジェクトにSearchEntityを追加しました:

@Data
public class SearchEntity {
 
 private long total; //検索結果のレコード総数
 private int totalPages; //クエリ結果の総ページ数
 private List<SkuInfo> rows; //検索結果のコレクション
 public SearchEntity() {
 }
 public SearchEntity(List<SkuInfo> rows, long total, int totalPages) {
 this.rows = rows;
 this.total = total;
 this.totalPages = totalPages;
 }
}

あとは、検索マイクロサービスに適切なコードを書くだけです。

@GetMapping
public Result<SearchEntity> searchByKeywords(@RequestParam(required = false)String keywords) {
 SearchEntity searchEntity = skuEsService.searchByKeywords(keywords);
 return new Result<>(true,StatusCode.OK,"キーワード検索の成功によると、",searchEntity);
}
---------------------------------------------------------------------------------------------------
@Override
public SearchEntity searchByKeywords(String keywords) {
 NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
 if (!StringUtils.isEmpty(keywords)) {
 nativeSearchQueryBuilder.withQuery(QueryBuilders.queryStringQuery(keywords).field("name"));
 }
 AggregatedPage<SkuInfo> skuInfos = elasticsearchTemplate
 .queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class);
 List<SkuInfo> content = skuInfos.getContent();
 return new SearchEntity(content,skuInfos.getTotalElements(),skuInfos.getTotalPages());
}

"http://localhost:18085"/search?keywords= その後、プロジェクトを立ち上げてアクセスしたところ、FAILED TO MAPが報告され、エラーメッセージの中に次のようなものがありました:

おそらくこれは、LocalDateTimeに何か問題があったことを意味し、Dateクラスがあまりよくなかったので、LocaDateTimeに変更しました。

/**
* LocalDateTimeが複数のFieldに分かれていないことを実現するために、最後の2つのアノテーションのみ使用できるが、フォーマットが正しくない。
* フォーマットとタイムゾーンを指定するために、最初の2つのアノテーションを追加する必要がある。
**/
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss || yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime createTime;//作成時間
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss || yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime updateTime;//更新時間

もう一度インポートしてください。

フォーマットがうまくいったので、もう一度テストしてみましょう。

わかりました!

シャオミモールのトップで商品を検索すると、下にカテゴリーが表示され、商品を絞り込むことができます。また、ショッピングモールのテーブルデザインにcategoryNameというフィールドがあります。次のステップは、分類統計のためのデータから検索を実装することです。

MySQLの代わりにElasticsearchを使用することで、図のような効果を得ることができます。

SearchEntity を修正して、categoryList フィールドを追加します:

private List<String> categoryList; //分類コレクション

SkuEsServiceImplのsearchByKeywordsメソッドを修正し、グループ統計用のコードを追加します:

public SearchEntity searchByKeywords(String keywords) {
 NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
 if (!StringUtils.isEmpty(keywords)) {
 nativeSearchQueryBuilder.withQuery(QueryBuilders.queryStringQuery(keywords).field("name"));
 //terms: Create a new aggregation with the given name.
 nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("categories_grouping")
 .field("categoryName"));
 }
 NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
 AggregatedPage<SkuInfo> skuInfos = elasticsearchTemplate.queryForPage(nativeSearchQuery, SkuInfo.class);
 StringTerms stringTerms = skuInfos.getAggregations().get("categories_grouping");
 List<String> categoryList = new ArrayList<>();
 for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
 categoryList.add(bucket.getKeyAsString());
 }
 return new SearchEntity(skuInfos.getTotalElements(),skuInfos.getTotalPages(),
 categoryList,skuInfos.getContent());
}

もう一度テストしてください:

OK!統計のグループ化が実装されました。

概要

本記事では、主にElasticsearchの環境構築から、データをESにインポートするところまで書きます。最後に、キーワード検索と分類統計の機能を実装します。

コード:

Read next

Notes PowerDesignerはSQLiteをサポートしている!

新しいシステムのデータベースを設計するために使用します。これには軽量のSQLiteを使いたいと思っています。私はいつもmysqlを使っているので、今回は多かれ少なかれ同じようにSQLiteを使えるはずです。そして、データベースの設計がクリアになったら、Navicat Premium 12ツールを使ってデータベースファイルpig_demoを作成しました。

Jan 4, 2021 · 2 min read