武漢網(wǎng)站建設(shè)S小蝌蚪互聯(lián)搜索引擎優(yōu)化名詞解釋
初識(shí)elasticsearch?
?
?
?
?
???.
?
?
?
?
安裝elasticsearch
1.部署單點(diǎn)es
1.1.創(chuàng)建網(wǎng)絡(luò)
因?yàn)槲覀冞€需要部署kibana容器,因此需要讓es和kibana容器互聯(lián)。這里先創(chuàng)建一個(gè)網(wǎng)絡(luò):
docker network create es-net
1.2.加載鏡像
這里我們采用elasticsearch的7.12.1版本的鏡像,這個(gè)鏡像體積非常大,接近1G。不建議大家自己pull。
課前資料提供了鏡像的tar包:
大家將其上傳到虛擬機(jī)中,然后運(yùn)行命令加載即可:
# 導(dǎo)入數(shù)據(jù)
docker load -i es.tar
同理還有kibana
的tar包也需要這樣做。
1.3.運(yùn)行
運(yùn)行docker命令,部署單點(diǎn)es:
docker run -d \--name es \-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \-e "discovery.type=single-node" \-v es-data:/usr/share/elasticsearch/data \-v es-plugins:/usr/share/elasticsearch/plugins \--privileged \--network es-net \-p 9200:9200 \-p 9300:9300 \
elasticsearch:7.12.1
命令解釋:
-
-e "cluster.name=es-docker-cluster"
:設(shè)置集群名稱 -
-e "http.host=0.0.0.0"
:監(jiān)聽的地址,可以外網(wǎng)訪問 -
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"
:內(nèi)存大小 -
-e "discovery.type=single-node"
:非集群模式 -
-v es-data:/usr/share/elasticsearch/data
:掛載邏輯卷,綁定es的數(shù)據(jù)目錄 -
-v es-logs:/usr/share/elasticsearch/logs
:掛載邏輯卷,綁定es的日志目錄 -
-v es-plugins:/usr/share/elasticsearch/plugins
:掛載邏輯卷,綁定es的插件目錄 -
--privileged
:授予邏輯卷訪問權(quán) -
--network es-net
:加入一個(gè)名為es-net的網(wǎng)絡(luò)中 -
-p 9200:9200
:端口映射配置
在瀏覽器中輸入:http://192.168.150.101:9200 即可看到elasticsearch的響應(yīng)結(jié)果:
2.部署kibana
kibana可以給我們提供一個(gè)elasticsearch的可視化界面,便于我們學(xué)習(xí)。
2.1.部署
運(yùn)行docker命令,部署kibana
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1
-
--network es-net
:加入一個(gè)名為es-net的網(wǎng)絡(luò)中,與elasticsearch在同一個(gè)網(wǎng)絡(luò)中 -
-e ELASTICSEARCH_HOSTS=http://es:9200"
:設(shè)置elasticsearch的地址,因?yàn)閗ibana已經(jīng)與elasticsearch在一個(gè)網(wǎng)絡(luò),因此可以用容器名直接訪問elasticsearch -
-p 5601:5601
:端口映射配置
kibana啟動(dòng)一般比較慢,需要多等待一會(huì),可以通過命令:
docker logs -f kibana
查看運(yùn)行日志,當(dāng)查看到下面的日志,說明成功:
2.2.DevTools
kibana中提供了一個(gè)DevTools界面:
這個(gè)界面中可以編寫DSL來操作elasticsearch。并且對(duì)DSL語句有自動(dòng)補(bǔ)全功能。
3.安裝IK分詞器
3.1.在線安裝ik插件(較慢)
# 進(jìn)入容器內(nèi)部
docker exec -it elasticsearch /bin/bash
?
# 在線下載并安裝
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
?
#退出
exit
#重啟容器
docker restart elasticsearch
3.2.離線安裝ik插件(推薦)
1)查看數(shù)據(jù)卷目錄
安裝插件需要知道elasticsearch的plugins目錄位置,而我們用了數(shù)據(jù)卷掛載,因此需要查看elasticsearch的數(shù)據(jù)卷目錄,通過下面命令查看:
docker volume inspect es-plugins
顯示結(jié)果:
[{"CreatedAt": "2022-05-06T10:06:34+08:00","Driver": "local","Labels": null,"Mountpoint": "/var/lib/docker/volumes/es-plugins/_data","Name": "es-plugins","Options": null,"Scope": "local"}
]
說明plugins目錄被掛載到了:/var/lib/docker/volumes/es-plugins/_data
這個(gè)目錄中。
2)解壓縮分詞器安裝包
下面我們需要把課前資料中的ik分詞器解壓縮,重命名為ik
3)上傳到es容器的插件數(shù)據(jù)卷中
也就是/var/lib/docker/volumes/es-plugins/_data
:
4)重啟容器
# 4、重啟容器
docker restart es
# 查看es日志
docker logs -f es
5)測(cè)試:
IK分詞器包含兩種模式:
-
ik_smart
:最少切分 -
ik_max_word
:最細(xì)切分
GET /_analyze
{"analyzer": "ik_max_word","text": "黑馬程序員學(xué)習(xí)java太棒了"
}
結(jié)果:
{"tokens" : [{"token" : "黑馬","start_offset" : 0,"end_offset" : 2,"type" : "CN_WORD","position" : 0},{"token" : "程序員","start_offset" : 2,"end_offset" : 5,"type" : "CN_WORD","position" : 1},{"token" : "程序","start_offset" : 2,"end_offset" : 4,"type" : "CN_WORD","position" : 2},{"token" : "員","start_offset" : 4,"end_offset" : 5,"type" : "CN_CHAR","position" : 3},{"token" : "學(xué)習(xí)","start_offset" : 5,"end_offset" : 7,"type" : "CN_WORD","position" : 4},{"token" : "java","start_offset" : 7,"end_offset" : 11,"type" : "ENGLISH","position" : 5},{"token" : "太棒了","start_offset" : 11,"end_offset" : 14,"type" : "CN_WORD","position" : 6},{"token" : "太棒","start_offset" : 11,"end_offset" : 13,"type" : "CN_WORD","position" : 7},{"token" : "了","start_offset" : 13,"end_offset" : 14,"type" : "CN_CHAR","position" : 8}]
}
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
GET /hotel/_search
{"query": {"match_all": {}},"sort": [{"score": {"order": "desc"}},{"price": {"order": "asc"}}]
}
?
GET /hotel/_search
{"query": {"match_all": {}},"sort": [{"_geo_distance": {"location": "31.034661,121.612282","order": "asc","unit": "km"}}]
}
?
?
?
#高亮查詢,默認(rèn)情況下,ES搜索字段必須與高亮字段一致
GET /hotel/_search
{"query": {"match": {"all":"如家"}},"highlight": {"fields": {"name": {"require_field_match": "false"}}}
}
?
?
?
?controller
@RestController
@RequestMapping("/hotel")
public class HotelController {@Autowiredprivate IHotelService hotelService;@PostMapping("/list")public PageResult list(@RequestBody RequestParams params) throws IOException {System.out.println(params);PageResult pageResult = hotelService.search(params);return pageResult;}
}
?service
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {@Autowiredprivate RestHighLevelClient client;@Overridepublic PageResult search(RequestParams params) throws IOException {//連接elasticsearchthis.client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.136.150:9200")));//1.準(zhǔn)備ResquestSearchRequest request = new SearchRequest("hotel");//2.組織DSL參數(shù)buidBasicQuery(params, request);//地理位置排序String location = params.getLocation();if(!StringUtils.isEmpty(location)){request.source().sort(SortBuilders.geoDistanceSort("location",new GeoPoint(location)).order(SortOrder.ASC).unit(DistanceUnit.KILOMETERS));}String sortBy = params.getSortBy();if(!sortBy.equals("default")){request.source().sort(sortBy);}//分頁Integer page = params.getPage();Integer size = params.getSize();if(page != null&& size !=null){request.source().from((page-1)*size).size(size);}//地理位置//3.發(fā)送請(qǐng)求SearchResponse response = client.search(request, RequestOptions.DEFAULT);System.out.println(response);//4.解析結(jié)果PageResult pageResult = handleResponse(response);//關(guān)閉連接this.client.close();return pageResult;}private void buidBasicQuery(RequestParams params, SearchRequest request) {//過濾條件BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();String key = params.getKey();if(!StringUtils.isEmpty(key)){boolQuery.must(QueryBuilders.matchQuery("all",key));}else{boolQuery.must(QueryBuilders.matchAllQuery());}//品牌String brand = params.getBrand();if(!StringUtils.isEmpty(brand)){boolQuery.must(QueryBuilders.termQuery("brand",brand));}//城市String city = params.getCity();if(!StringUtils.isEmpty(city)){boolQuery.must(QueryBuilders.termQuery("city",city));}//星級(jí)String starName = params.getStarName();if(!StringUtils.isEmpty(starName)){boolQuery.must(QueryBuilders.termQuery("starName",starName));}//價(jià)錢Integer minPrice = params.getMinPrice();Integer maxPrice = params.getMaxPrice();if(minPrice != null && maxPrice != null){boolQuery.filter(QueryBuilders.rangeQuery("price").gte(minPrice).lte(maxPrice));}//算分控制FunctionScoreQueryBuilder functionScoreQuery =QueryBuilders.functionScoreQuery(//原始查詢boolQuery,//function score數(shù)組new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{//其中的一個(gè)function score元素new FunctionScoreQueryBuilder.FilterFunctionBuilder(//過濾條件QueryBuilders.termQuery("isAD",true),//算分函數(shù)ScoreFunctionBuilders.weightFactorFunction(10))});request.source().query(functionScoreQuery);}//解析結(jié)果函數(shù)private PageResult handleResponse(SearchResponse response){SearchHits searchHits = response.getHits();//1.查詢總條數(shù)Long total = searchHits.getTotalHits().value;//2.查詢的結(jié)果數(shù)組SearchHit[] hits = searchHits.getHits();//3.遍歷List<HotelDoc> hotels = new ArrayList<>();for (SearchHit hit : hits) {//獲取文檔sourceString json = hit.getSourceAsString();//反序列化HotelDoc hotelDoc = JSON.parseObject(json,HotelDoc.class);//獲取排序值Object[] sortValues = hit.getSortValues();if(sortValues.length > 0){hotelDoc.setDistance(sortValues[0]);}//獲取高亮結(jié)果Map<String, HighlightField> highlightFields = hit.getHighlightFields();if(!CollectionUtils.isEmpty(highlightFields)){//根據(jù)字段名獲取高亮結(jié)果HighlightField highlightField = highlightFields.get("name");if(highlightField != null){//獲取高亮值String name = highlightField.getFragments()[0].string();hotelDoc.setName(name);}}//打印hotels.add(hotelDoc);}//4.構(gòu)造返回值PageResult pageResult = new PageResult(total,hotels);return pageResult;}
}
?pojo
@Data
@NoArgsConstructor
public class HotelDoc {private Long id;private String name;private String address;private Integer price;private Integer score;private String brand;private String city;private String starName;private String business;private String location;private String pic;private Object distance;private Boolean isAD;public HotelDoc(Hotel hotel) {this.id = hotel.getId();this.name = hotel.getName();this.address = hotel.getAddress();this.price = hotel.getPrice();this.score = hotel.getScore();this.brand = hotel.getBrand();this.city = hotel.getCity();this.starName = hotel.getStarName();this.business = hotel.getBusiness();this.location = hotel.getLatitude() + ", " + hotel.getLongitude();this.pic = hotel.getPic();}
}
?
?
# metrics聚合
GET /hotel/_search
{"size": 0,"aggs": {"brandAgg": {"terms": {"field": "brand","size": 20,"order": {"scoreAgg.avg": "desc"}},"aggs": {"scoreAgg": {"stats": {"field": "score"}}}}}
}
?
?
Controller
@PostMapping("/filters")public Map<String, List<String>> filters(@RequestBody RequestParams params) throws IOException {System.out.println("filter:"+params);Map<String, List<String>> map = hotelService.filters(params);System.out.println("Map:"+map);return map;}
?Service
@Overridepublic Map<String, List<String>> filters(RequestParams params) throws IOException {//1.創(chuàng)建RequestSearchRequest request = new SearchRequest("hotel");//2.組織DSL語句//2.1 querybuidBasicQuery(params, request);//2.2 設(shè)置sizerequest.source().size(0);//2.3 聚合buildAggregation(request);//3.發(fā)送請(qǐng)求SearchResponse response = client.search(request,RequestOptions.DEFAULT);//4.解析結(jié)果Map<String,List<String>> map = new HashMap<>();//4.1.根據(jù)城市名稱,獲取聚合結(jié)果List<String> cityList = getAggByName(response,"cityAgg");map.put("城市",cityList);//4.2.根據(jù)星級(jí)名稱,獲取聚合結(jié)果List<String> starList = getAggByName(response,"starAgg");map.put("星級(jí)",starList);///4.3.根據(jù)品牌名稱,獲取聚合結(jié)果List<String> brandList = getAggByName(response,"brandAgg");map.put("品牌",brandList);return map;}private void buildAggregation(SearchRequest request) {//聚合城市request.source().aggregation(AggregationBuilders.terms("cityAgg").field("city").size(100));//聚合星級(jí)request.source().aggregation(AggregationBuilders.terms("starAgg").field("starName").size(100));//聚合品牌request.source().aggregation(AggregationBuilders.terms("brandAgg").field("brand").size(100));}private List<String> getAggByName(SearchResponse response, String aggName) {Aggregations aggregations = response.getAggregations();Terms brandTerms = aggregations.get(aggName);//獲取桶List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();//遍歷List<String> list = new ArrayList<>();for (Terms.Bucket bucket : buckets) {//獲取keyString key = bucket.getKeyAsString();list.add(key);}return list;}
POST /_analyze
{"text": ["如家酒店還不錯(cuò)"],"analyzer": "pinyin"
}
?
POST /test/_analyze
{"text": ["如家酒店還不錯(cuò)"],"analyzer": "my_analyzer"
}# 自定義拼音分詞器
PUT /test
{"settings": {"analysis": {"analyzer": { "my_analyzer": { "tokenizer": "ik_max_word","filter": "py"}},"filter": {"py": { "type": "pinyin","keep_full_pinyin": false,"keep_joined_full_pinyin": true,"keep_original": true,"limit_first_letter_length": 16,"remove_duplicated_term": true,"none_chinese_pinyin_tokenize": false}}}},"mappings": {"properties": {"name":{"type": "text","analyzer": "my_analyzer"}}}
}POST /test/_doc/1
{"id": 1,"name": "獅子"
}
POST /test/_doc/2
{"id": 2,"name": "虱子"
}GET /test/_search
{"query": {"match": {"name": "shizi"}}
}
?
// 酒店數(shù)據(jù)索引庫
PUT /hotel
{"settings": {"analysis": {"analyzer": {"text_anlyzer": {"tokenizer": "ik_max_word","filter": "py"},"completion_analyzer": {"tokenizer": "keyword","filter": "py"}},"filter": {"py": {"type": "pinyin","keep_full_pinyin": false,"keep_joined_full_pinyin": true,"keep_original": true,"limit_first_letter_length": 16,"remove_duplicated_term": true,"none_chinese_pinyin_tokenize": false}}}},"mappings": {"properties": {"id":{"type": "keyword"},"name":{"type": "text","analyzer": "text_anlyzer","search_analyzer": "ik_smart","copy_to": "all"},"address":{"type": "keyword","index": false},"price":{"type": "integer"},"score":{"type": "integer"},"brand":{"type": "keyword","copy_to": "all"},"city":{"type": "keyword"},"starName":{"type": "keyword"},"business":{"type": "keyword","copy_to": "all"},"location":{"type": "geo_point"},"pic":{"type": "keyword","index": false},"all":{"type": "text","analyzer": "text_anlyzer","search_analyzer": "ik_smart"},"suggestion":{"type": "completion","analyzer": "completion_analyzer"}}}
}GET /hotel/_search
{"suggest": {"title_suggest": {"text": "地", "completion": {"field": "suggestion", "skip_duplicates": true, "size": 10 }}}
}
?Controller
@GetMapping("/suggestion")public List<String> suggestion(String key) throws IOException {System.out.println("suggestion接口被訪問了:"+key);List<String> list = hotelService.suggestion(key);return list;}
?Service
@Overridepublic List<String> suggestion(String key) throws IOException {//1.創(chuàng)建RequestSearchRequest request = new SearchRequest("hotel");//2.組織DSL語句request.source().suggest(new SuggestBuilder().addSuggestion("hotelSuggestion",SuggestBuilders.completionSuggestion("suggestion").prefix(key).skipDuplicates(true).size(100)));//3.發(fā)送請(qǐng)求SearchResponse response = client.search(request,RequestOptions.DEFAULT);//4.解析數(shù)據(jù)Suggest suggest = response.getSuggest();//根據(jù)名稱獲取補(bǔ)全結(jié)果CompletionSuggestion suggestion = suggest.getSuggestion("hotelSuggestion");//獲取option并遍歷List<String> list = new ArrayList<>();for (CompletionSuggestion.Entry.Option option : suggestion.getOptions()) {//獲取一個(gè)option中的text,也就是補(bǔ)全的詞條String text = option.getText().string();list.add(text);}return list;}
?
發(fā)送MQ消息
@PostMappingpublic void saveHotel(@RequestBody Hotel hotel){hotelService.save(hotel);rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,MqConstants.HOTEL_INSERT_KEY,hotel.getId());}@PutMapping()public void updateById(@RequestBody Hotel hotel){if (hotel.getId() == null) {throw new InvalidParameterException("id不能為空");}hotelService.updateById(hotel);rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,MqConstants.HOTEL_INSERT_KEY,hotel.getId());}@DeleteMapping("/{id}")public void deleteById(@PathVariable("id") Long id) {hotelService.removeById(id);rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,MqConstants.HOTEL_DELETE_KEY,id);}
監(jiān)聽MQ消息?
public class MqConstants {/*** 交換機(jī)*/public final static String HOTEL_EXCHANGE = "hotel.topic";/*** 監(jiān)聽新增和修改的隊(duì)列*/public final static String HOTEL_INSERT_QUEUE = "hotel.insert.queue";/*** 監(jiān)聽刪除的隊(duì)列*/public final static String HOTEL_DELETE_QUEUE = "hotel.delete.queue";/*** 新增或修改的RoutingKey*/public final static String HOTEL_INSERT_KEY = "hotel.insert";/*** 刪除的RoutingKey*/public final static String HOTEL_DELETE_KEY = "hotel.delete";
}
@Configuration
public class MqConfig {//聲明一個(gè)交換機(jī)@Beanpublic TopicExchange topicExchange(){return new TopicExchange(MqConstants.HOTEL_EXCHANGE,true,false);}//定義隊(duì)列@Beanpublic Queue insertQueue(){return new Queue(MqConstants.HOTEL_INSERT_QUEUE,true);}@Beanpublic Queue deleteQueue(){return new Queue(MqConstants.HOTEL_DELETE_QUEUE,true);}//將隊(duì)列與交換機(jī)進(jìn)行綁定@Beanpublic Binding insertQueueBanding(){return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);}@Beanpublic Binding deleteQueueBanding(){return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);}
}
@PostMappingpublic void saveHotel(@RequestBody Hotel hotel){hotelService.save(hotel);rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,MqConstants.HOTEL_INSERT_KEY,hotel.getId());}@PutMapping()public void updateById(@RequestBody Hotel hotel){if (hotel.getId() == null) {throw new InvalidParameterException("id不能為空");}hotelService.updateById(hotel);rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,MqConstants.HOTEL_INSERT_KEY,hotel.getId());}@DeleteMapping("/{id}")public void deleteById(@PathVariable("id") Long id) {hotelService.removeById(id);rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,MqConstants.HOTEL_DELETE_KEY,id);}
部署es集群
我們會(huì)在單機(jī)上利用docker容器運(yùn)行多個(gè)es實(shí)例來模擬es集群。不過生產(chǎn)環(huán)境推薦大家每一臺(tái)服務(wù)節(jié)點(diǎn)僅部署一個(gè)es的實(shí)例。
部署es集群可以直接使用docker-compose來完成,但這要求你的Linux虛擬機(jī)至少有4G的內(nèi)存空間
創(chuàng)建es集群
首先編寫一個(gè)docker-compose文件,內(nèi)容如下:
version: '2.2'
services:es01:image: elasticsearch:7.12.1container_name: es01environment:- node.name=es01- cluster.name=es-docker-cluster- discovery.seed_hosts=es02,es03- cluster.initial_master_nodes=es01,es02,es03- "ES_JAVA_OPTS=-Xms512m -Xmx512m"volumes:- data01:/usr/share/elasticsearch/dataports:- 9200:9200networks:- elastices02:image: elasticsearch:7.12.1container_name: es02environment:- node.name=es02- cluster.name=es-docker-cluster- discovery.seed_hosts=es01,es03- cluster.initial_master_nodes=es01,es02,es03- "ES_JAVA_OPTS=-Xms512m -Xmx512m"volumes:- data02:/usr/share/elasticsearch/dataports:- 9201:9200networks:- elastices03:image: elasticsearch:7.12.1container_name: es03environment:- node.name=es03- cluster.name=es-docker-cluster- discovery.seed_hosts=es01,es02- cluster.initial_master_nodes=es01,es02,es03- "ES_JAVA_OPTS=-Xms512m -Xmx512m"volumes:- data03:/usr/share/elasticsearch/datanetworks:- elasticports:- 9202:9200
volumes:data01:driver: localdata02:driver: localdata03:driver: localnetworks:elastic:driver: bridge
es運(yùn)行需要修改一些linux系統(tǒng)權(quán)限,修改/etc/sysctl.conf
文件
vi /etc/sysctl.conf
添加下面的內(nèi)容:
vm.max_map_count=262144
然后執(zhí)行命令,讓配置生效:
sysctl -p
通過docker-compose啟動(dòng)集群:
docker-compose up -d
4.2.集群狀態(tài)監(jiān)控
kibana可以監(jiān)控es集群,不過新版本需要依賴es的x-pack 功能,配置比較復(fù)雜。
這里推薦使用cerebro來監(jiān)控es集群狀態(tài),官方網(wǎng)址:GitHub - lmenezes/cerebro
課前資料已經(jīng)提供了安裝包:
?解壓即可使用,非常方便。
解壓好的目錄如下:
進(jìn)入對(duì)應(yīng)的bin目錄:
?雙擊其中的cerebro.bat文件即可啟動(dòng)服務(wù)。
?訪問http://localhost:9000 即可進(jìn)入管理界面:
?輸入你的elasticsearch的任意節(jié)點(diǎn)的地址和端口,點(diǎn)擊connect即可:
?綠色的條,代表集群處于綠色(健康狀態(tài))。
創(chuàng)建索引庫
1)利用kibana的DevTools創(chuàng)建索引庫
在DevTools中輸入指令:
PUT /itcast
{"settings": {"number_of_shards": 3, // 分片數(shù)量"number_of_replicas": 1 // 副本數(shù)量},"mappings": {"properties": {// mapping映射定義 ...}}
}
2)利用cerebro創(chuàng)建索引庫
利用cerebro還可以創(chuàng)建索引庫:
?填寫索引庫信息:
?點(diǎn)擊右下角的create按鈕:
查看分片效果
回到首頁,即可查看索引庫分片效果:
?
?
?
?
?
?
?
?
?
??