基於Java、Kafka、ElasticSearch的搜尋框架的設計與實現
Jkes是一個基於Java、Kafka、ElasticSearch的搜尋框架。Jkes提供了註解驅動的JPA風格的物件/文件對映,使用REST API用於文件搜尋。
專案主頁:https://github.com/chaokunyang/jkes
安裝
可以參考jkes-integration-test
專案快速掌握jkes框架的使用方法。jkes-integration-test
是我們用來測試功能完整性的一個Spring Boot Application。
- 安裝
jkes-index-connector
和jkes-delete-connector
到Kafka Connect類路徑 - 安裝 Smart Chinese Analysis Plugin
sudo bin/elasticsearch-plugin install analysis-smartcn
配置
- 引入jkes-spring-data-jpa依賴
- 新增配置
@EnableAspectJAutoProxy @EnableJkes @Configuration public class JkesConfig { @Bean public PlatformTransactionManager transactionManager(EntityManagerFactory factory, EventSupport eventSupport) { return new SearchPlatformTransactionManager(new JpaTransactionManager(factory), eventSupport); } }
提供JkesProperties Bean
@Component @Configuration public class JkesConf extends DefaultJkesPropertiesImpl { @PostConstruct public void setUp() { Config.setJkesProperties(this); } @Override public String getKafkaBootstrapServers() { return "k1-test.com:9292,k2-test.com:9292,k3-test.com:9292"; } @Override public String getKafkaConnectServers() { return "http://k1-test.com:8084,http://k2-test.com:8084,http://k3-test.com:8084"; } @Override public String getEsBootstrapServers() { return "http://es1-test.com:9200,http://es2-test.com:9200,http://es3-test.com:9200"; } @Override public String getDocumentBasePackage() { return "com.timeyang.jkes.integration_test.domain"; } @Override public String getClientId() { return "integration_test"; } }
這裡可以很靈活,如果使用Spring Boot,可以使用@ConfigurationProperties
提供配置
增加索引管理端點 因為我們不知道客戶端使用的哪種web技術,所以索引端點需要在客戶端新增。比如在Spring MVC
中,可以按照如下方式新增索引端點
@RestController @RequestMapping("/api/search") public class SearchEndpoint { private Indexer indexer; @Autowired public SearchEndpoint(Indexer indexer) { this.indexer = indexer; } @RequestMapping(value = "/start_all", method = RequestMethod.POST) public void startAll() { indexer.startAll(); } @RequestMapping(value = "/start/{entityClassName:.+}", method = RequestMethod.POST) public void start(@PathVariable("entityClassName") String entityClassName) { indexer.start(entityClassName); } @RequestMapping(value = "/stop_all", method = RequestMethod.PUT) public Map<String, Boolean> stopAll() { return indexer.stopAll(); } @RequestMapping(value = "/stop/{entityClassName:.+}", method = RequestMethod.PUT) public Boolean stop(@PathVariable("entityClassName") String entityClassName) { return indexer.stop(entityClassName); } @RequestMapping(value = "/progress", method = RequestMethod.GET) public Map<String, IndexProgress> getProgress() { return indexer.getProgress(); } }
快速開始
索引API
使用com.timeyang.jkes.core.annotation
包下相關注解標記實體
@lombok.Data @Entity @Document public class Person extends AuditedEntity { // @Id will be identified automatically // @Field(type = FieldType.Long) @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @MultiFields( mainField = @Field(type = FieldType.Text), otherFields = { @InnerField(suffix = "raw", type = FieldType.Keyword), @InnerField(suffix = "english", type = FieldType.Text, analyzer = "english") } ) private String name; @Field(type = FieldType.Keyword) private String gender; @Field(type = FieldType.Integer) private Integer age; // don't add @Field to test whether ignored // @Field(type = FieldType.Text) private String description; @Field(type = FieldType.Object) @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "group_id") private PersonGroup personGroup; }
@lombok.Data @Entity @Document(type = "person_group", alias = "person_group_alias") public class PersonGroup extends AuditedEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String interests; @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "personGroup", orphanRemoval = true) private List<Person> persons; private String description; @DocumentId @Field(type = FieldType.Long) public Long getId() { return id; } @MultiFields( mainField = @Field(type = FieldType.Text), otherFields = { @InnerField(suffix = "raw", type = FieldType.Keyword), @InnerField(suffix = "english", type = FieldType.Text, analyzer = "english") } ) public String getName() { return name; } @Field(type = FieldType.Text) public String getInterests() { return interests; } @Field(type = FieldType.Nested) public List<Person> getPersons() { return persons; } /** * 不加Field註解,測試序列化時是否忽略 */ public String getDescription() { return description; } }
當更新實體時,文件會被自動索引到ElasticSearch;刪除實體時,文件會自動從ElasticSearch刪除。
搜尋API
啟動搜尋服務jkes-search-service,搜尋服務是一個Spring Boot Application,提供rest搜尋api,預設執行在9000埠。
URI query
curl -XPOST localhost:9000/api/v1/integration_test_person_group/person_group/_search?from=3&size=10
Nested query
integration_test_person_group/person_group/_search?from=0&size=10 { "query": { "nested": { "path": "persons", "score_mode": "avg", "query": { "bool": { "must": [ { "range": { "persons.age": { "gt": 5 } } } ] } } } } }
match query
integration_test_person_group/person_group/_search?from=0&size=10 { "query": { "match": { "interests": "Hadoop" } } }
bool query
{ "query": { "bool" : { "must" : { "match" : { "interests" : "Hadoop" } }, "filter": { "term" : { "name.raw" : "name0" } }, "should" : [ { "match" : { "interests" : "Flink" } }, { "nested" : { "path" : "persons", "score_mode" : "avg", "query" : { "bool" : { "must" : [ { "match" : {"persons.name" : "name40"} }, { "match" : {"persons.interests" : "interests"} } ], "must_not" : { "range" : { "age" : { "gte" : 50, "lte" : 60 } } } } } } } ], "minimum_should_match" : 1, "boost" : 1.0 } } }
Source filtering
integration_test_person_group/person_group/_search { "_source": false, "query" : { "match" : { "name" : "name17" } } }
integration_test_person_group/person_group/_search { "_source": { "includes": [ "name", "persons.*" ], "excludes": [ "date*", "version", "persons.age" ] }, "query" : { "match" : { "name" : "name17" } } }
prefix
integration_test_person_group/person_group/_search { "query": { "prefix" : { "name" : "name" } } }
wildcard
integration_test_person_group/person_group/_search { "query": { "wildcard" : { "name" : "name*" } } }
regexp
integration_test_person_group/person_group/_search { "query": { "regexp":{ "name": "na.*17" } } }
Jkes工作原理
索引工作原理:
- 應用啟動時,Jkes掃描所有標註
@Document
註解的實體,為它們構建後設資料。 - 基於構建的後設資料,建立
index
和mapping
Json格式的配置,然後通過ElasticSearch Java Rest Client
將建立/更新index
配置。 - 為每個文件建立/更新
Kafka ElasticSearch Connector
,用於建立/更新文件 - 為整個專案啟動/更新
Jkes Deleter Connector
,用於刪除文件 - 攔截資料操作方法。將
* save(*)
方法返回的資料包裝為SaveEvent
儲存到EventContainer
;使用(* delete*(..)
方法的引數,生成一個DeleteEvent/DeleteAllEvent
儲存到EventContainer
。 - 攔截事務。在事務提交後使用
JkesKafkaProducer
傳送SaveEvent
中的實體到Kafka,Kafka會使用我們提供的JkesJsonSerializer
序列化指定的資料,然後傳送到Kafka。 - 與
SaveEvent
不同,DeleteEvent
會直接被序列化,然後傳送到Kafka,而不是隻傳送一份資料 - 與
SaveEvent
和DeleteEvent
不同,DeleteAllEvent
不會傳送資料到Kafka,而是直接通過ElasticSearch Java Rest Client
刪除相應的index
,然後重建該索引,重啟Kafka ElasticSearch Connector
查詢工作原理:
- 查詢服務通過rest api提供
- 我們沒有直接使用ElasticSearch進行查詢,因為我們需要在後續版本使用機器學習進行搜尋排序,而直接與ElasticSearch進行耦合,會增加搜尋排序API的接入難度
- 查詢服務是一個Spring Boot Application,使用docker打包為映象
- 查詢服務提供多版本API,用於API進化和相容
- 查詢服務解析
json
請求,進行一些預處理後,使用ElasticSearch Java Rest Client
轉發到ElasticSearch,將得到的響應進行解析,進一步處理後返回到客戶端。 - 為了便於客戶端人員開發,查詢服務提供了一個查詢UI介面,開發人員可以在這個頁面得到預期結果後再把json請求體複製到程式中。
流程圖
模組介紹
jkes-core
jkes-core
是整個jkes
的核心部分。主要包括以下功能:
annotation
包提供了jkes的核心註解elasticsearch
包封裝了elasticsearch
相關的操作,如為所有的文件建立/更新索引,更新mappingkafka
包提供了Kafka 生產者,Kafka Json Serializer,Kafka Connect Clientmetadata
包提供了核心的註解後設資料的構建與結構化模型event
包提供了事件模型與容器exception
包提供了常見的Jkes異常http
包基於Apache Http Client
封裝了常見的http json請求support
包暴露了Jkes核心配置支援util
包提供了一些工具類,便於開發。如:Asserts, ClassUtils, DocumentUtils, IOUtils, JsonUtils, ReflectionUtils, StringUtils
jkes-boot
jkes-boot
用於與一些第三方開源框架進行整合。
當前,我們通過jkes-spring-data-jpa
,提供了與spring data jpa
的整合。通過使用Spring的AOP機制,對Repository
方法進行攔截,生成SaveEvent/DeleteEvent/DeleteAllEvent
儲存到EventContainer
。通過使用我們提供的SearchPlatformTransactionManager
,對常用的事務管理器(如JpaTransactionManager
)進行包裝,提供事務攔截功能。
在後續版本,我們會提供與更多框架的整合。
jkes-spring-data-jpa
說明:
ContextSupport
類用於從bean工廠獲取Repository Bean
@EnableJkes
讓客戶端能夠輕鬆開啟Jkes的功能,提供了與Spring一致的配置模型EventSupport
處理事件的細節,在儲存和刪除資料時生成相應事件存放到EventContainer
,在事務提交和回滾時處理相應的事件SearchPlatformTransactionManager
包裝了客戶端的事務管理器,在事務提交和回滾時加入了回撥hook
audit
包提供了一個簡單的AuditedEntity
父類,方便新增審計功能,版本資訊可用於結合ElasticSearch
的版本機制保證不會索引過期文件資料exception
包封裝了常見異常intercept
包提供了AOP切點和切面index
包提供了全量索引
功能。當前,我們提供了基於執行緒池
的索引機制和基於ForkJoin
的索引機制。在後續版本,我們會重構程式碼,增加基於阻塞佇列
的生產者-消費者
模式,提供併發效能
jkes-services
jkes-services
主要用來提供一些服務。 目前,jkes-services
提供了以下服務:
jkes-delete-connector
jkes-delete-connector
是一個Kafka Connector
,用於從kafka叢集獲取索引刪除事件(DeleteEvent
),然後使用Jest Client
刪除ElasticSearch中相應的文件。- 藉助於Kafka Connect的rest admin api,我們輕鬆地實現了多租戶平臺上的文件刪除功能。只要為每個專案啟動一個
jkes-delete-connector
,就可以自動處理該專案的文件刪除工作。避免了每啟動一個新的專案,我們都得手動啟動一個Kafka Consumer來處理該專案的文件刪除工作。儘管可以通過正則訂閱來減少這樣的工作,但是還是非常不靈活
jkes-search-service
jkes-search-service
是一個restful的搜尋服務,提供了多版本的rest query api。查詢服務提供多版本API,用於API進化和相容jkes-search-service
目前支援URI風格的搜尋和JSON請求體風格的搜尋。- 我們沒有直接使用ElasticSearch進行查詢,因為我們需要在後續版本使用機器學習進行搜尋排序,而直接與ElasticSearch進行耦合,會增加搜尋排序的接入難度
- 查詢服務是一個Spring Boot Application,使用docker打包為映象
- 查詢服務解析
json
請求,進行一些預處理後,使用ElasticSearch Java Rest Client
轉發到ElasticSearch,將得到的響應進行解析,進一步處理後返回到客戶端。 - 為了便於客戶端人員開發,查詢服務提供了一個查詢UI介面,開發人員可以在這個頁面得到預期結果後再把json請求體複製到程式中。
後續,我們將會基於zookeeper
構建索引叢集,提供叢集索引管理功能
jkes-integration-test
jkes-integration-test
是一個基於Spring Boot整合測試專案,用於進行功能測試
。同時測量一些常見操作的吞吐率
開發
To build a development version you’ll need a recent version of Kafka. You can build jkes with Maven using the standard lifecycle phases.
Contribute
- Source Code: https://github.com/chaokunyang/jkes
- Issue Tracker: https://github.com/chaokunyang/jkes/issues
LICENSE
This project is licensed under Apache License 2.0.
相關文章
- elasticsearch實現基於拼音搜尋Elasticsearch
- 基於Elasticsearch實現搜尋建議Elasticsearch
- 基於Kafka和Elasticsearch構建實時站內搜尋功能的實踐KafkaElasticsearch
- Nebula 基於 ElasticSearch 的全文搜尋引擎的文字搜尋Elasticsearch
- 基於 Elasticsearch 的站內搜尋引擎實戰Elasticsearch
- Elasticsearch搜尋功能的實現(五)-- 實戰Elasticsearch
- 基於 Kafka 的實時數倉在搜尋的實踐應用Kafka
- Elasticsearch 實現簡單搜尋Elasticsearch
- Laravel + Elasticsearch 實現中文搜尋LaravelElasticsearch
- [計算機視覺]基於內容的影像搜尋實現計算機視覺
- 基於java的網路招聘系統的設計與實現Java
- 基於java的文章釋出系統的設計與實現Java
- 智慧搜尋模型預估框架Augur的建設與實踐模型框架
- 使用 Laravel Scout + ElasticSearch 實現全文搜尋LaravelElasticsearch
- 搜尋引擎ElasticSearch18_ElasticSearch程式設計操作5Elasticsearch程式設計
- 基於java的企業車輛管理系統的設計與實現Java
- Java畢業設計_基於MySQL網盤管理系統的設計與實現JavaMySql
- 基於android的智慧導診的設計與實現Android
- Elasticsearch 的配置與使用,為了全文搜尋Elasticsearch
- Elasticsearch(ES)的高階搜尋(DSL搜尋)(上篇)Elasticsearch
- Elasticsearch(ES)的高階搜尋(DSL搜尋)(下篇)Elasticsearch
- 基於java博網即時通訊軟體的設計與實現Java
- 白瑜慶:知乎基於Kubernetes的kafka平臺的設計和實現Kafka
- Elasticsearch 近實時搜尋的底層原理Elasticsearch
- 系統設計:如何設計一個類似於Tinder的基於位置的社交搜尋應用
- 基於Android的失物招領APP的設計與實現AndroidAPP
- Debezium結合Kafka Connect實時捕獲MySQL變更事件寫入Elasticsearch實現搜尋流程KafkaMySql事件Elasticsearch
- 基於java的大學生健康資訊管理系統的設計與實現Java
- 基於Java+SpringBoot+Mysql實現的古詩詞平臺功能設計與實現三JavaSpring BootMySql
- 基於區塊鏈的智慧鎖設計與實現區塊鏈
- 一圖簡看基於搜尋的問答機器人設計機器人
- 關於介面測試——自動化框架的設計與實現框架
- 基於java的陶瓷工廠進銷存管理系統的設計與實現Java
- 基於Android的音樂播放器的設計與實現Android播放器
- Elasticsearch常用搜尋Elasticsearch
- Elasticsearch——全文搜尋Elasticsearch
- elasticsearch搜尋商品Elasticsearch
- Elasticsearch 向量搜尋Elasticsearch
- 基於 Mysql 實現一個簡易版搜尋引擎MySql