Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("/product/search")
public class ElasticSearchController {
public class SearchController {
private final SearchService searchService;

@GetMapping()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,116 +1,24 @@
package ua.everybuy.routing.controller.system;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import ua.everybuy.database.entity.Advertisement;
import ua.everybuy.database.entity.AdvertisementDocument;
import ua.everybuy.database.repository.advertisement.AdvertisementRepository;
import ua.everybuy.routing.mapper.AdvertisementDocumentMapper;

import ua.everybuy.service.advertisement.search.ElasticsearchReindexService;
import java.io.IOException;
import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/product/search")
public class ElasticSearchIndexController {

private final AdvertisementRepository advertisementRepository;
private final AdvertisementDocumentMapper mapper;
private final RestHighLevelClient restHighLevelClient;

private static final String INDEX_NAME = "advertisements"; // Название индекса
private final ElasticsearchReindexService elasticsearchReindexService;

@PostMapping("/reindex")
@ResponseStatus(HttpStatus.ACCEPTED)
public String reindexAllAdvertisements() {
try {
createIndexIfNotExists();

DeleteByQueryRequest deleteRequest = new DeleteByQueryRequest(INDEX_NAME);
deleteRequest.setQuery(QueryBuilders.matchAllQuery());
deleteRequest.setConflicts("proceed");
restHighLevelClient.deleteByQuery(deleteRequest, RequestOptions.DEFAULT);

List<Advertisement> allAds = advertisementRepository.findAll();

BulkRequest bulkRequest = new BulkRequest();

ObjectMapper objectMapper = new ObjectMapper();

for (Advertisement ad : allAds) {
AdvertisementDocument doc = mapper.mapToDocument(ad);
doc.setId(ad.getId());

IndexRequest indexRequest = new IndexRequest(INDEX_NAME)
.id(String.valueOf(doc.getId()))
.source(objectMapper.writeValueAsString(doc), XContentType.JSON);

bulkRequest.add(indexRequest);
}

restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);

return "Successfully reindexed %d advertisements".formatted(allAds.size());

} catch (Exception e) {
throw new RuntimeException("Failed to reindex advertisements", e);
}
public String reindexAllAdvertisements() throws IOException {
return elasticsearchReindexService.reindexAllAdvertisements();
}
private void createIndexIfNotExists() throws IOException {
GetIndexRequest getIndexRequest = new GetIndexRequest(INDEX_NAME);
boolean exists = restHighLevelClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
if (exists) return;

CreateIndexRequest request = new CreateIndexRequest(INDEX_NAME);

String mappingJson = """
{
"settings": {
"analysis": {
"analyzer": {
"ukrainian": {
"type": "ukrainian"
}
}
}
},
"mappings": {
"properties": {
"id": { "type": "long" },
"title": { "type": "text", "analyzer": "ukrainian" },
"description": { "type": "text", "analyzer": "ukrainian" },
"price": { "type": "double" },
"creationDate": { "type": "date" },
"updateDate": { "type": "date" },
"isEnabled": { "type": "boolean" },
"userId": { "type": "long" },
"mainPhotoUrl": { "type": "text" },
"cityId": { "type": "long" },
"topSubCategoryId": { "type": "long" },
"lowSubCategoryId": { "type": "long" },
"productType": { "type": "keyword" },
"section": { "type": "keyword" }
}
}
}
""";

request.source(mappingJson, XContentType.JSON);
restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@
import lombok.RequiredArgsConstructor;
import org.mapstruct.Named;
import org.springframework.stereotype.Component;
import ua.everybuy.service.category.AdvertisementSubCategoryService;
import ua.everybuy.service.category.SubCategoryService;
import ua.everybuy.database.entity.LowLevelSubCategory;
import ua.everybuy.database.entity.TopLevelSubCategory;
import ua.everybuy.routing.dto.request.CategoryRequest;

@Component
@RequiredArgsConstructor
public class CategoryMappingHelper {
private final AdvertisementSubCategoryService advertisementSubCategoryService;
private final SubCategoryService subCategoryService;

@Named("getTopLevelSubCategory")
public <T extends CategoryRequest> TopLevelSubCategory getTopLevelSubCategory(T request) {
return advertisementSubCategoryService.getTopLevelSubCategory(request);
return subCategoryService.getTopLevelSubCategory(request);
}

@Named("getLowLevelSubCategory")
public <T extends CategoryRequest> LowLevelSubCategory getLowLevelSubCategory(T request) {
return advertisementSubCategoryService.getLowLevelSubCategory(request);
return subCategoryService.getLowLevelSubCategory(request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import lombok.RequiredArgsConstructor;
import org.mapstruct.Named;
import org.springframework.stereotype.Component;
import ua.everybuy.service.advertisement.DeliveryService;
import ua.everybuy.service.delivery.DeliveryService;
import ua.everybuy.database.entity.Advertisement;

import java.util.Set;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import ua.everybuy.service.delivery.DeliveryService;
import ua.everybuy.service.photo.PhotoService;
import ua.everybuy.database.entity.Advertisement;
import ua.everybuy.database.entity.AdvertisementPhoto;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import ua.everybuy.service.delivery.DeliveryService;
import ua.everybuy.service.photo.PhotoService;
import ua.everybuy.database.entity.Advertisement;
import ua.everybuy.database.entity.AdvertisementPhoto;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@
import ua.everybuy.service.advertisement.filter.sort.SortStrategyFactory;
import ua.everybuy.database.entity.Advertisement;
import ua.everybuy.database.repository.advertisement.AdvertisementRepository;
import ua.everybuy.database.repository.advertisement.spec.factory.AdvertisementDistinctTopCategorySpecificationFactory;
import ua.everybuy.database.repository.advertisement.spec.factory.AdvertisementSearchSpecificationFactory;
import ua.everybuy.routing.dto.PriceRangeDto;
import ua.everybuy.routing.dto.request.AdvertisementSearchParametersDto;
import ua.everybuy.routing.dto.response.FilteredAdvertisementsResponse;
import ua.everybuy.routing.dto.AdvertisementSearchResultDto;
import ua.everybuy.routing.mapper.AdvertisementFilterMapper;
import ua.everybuy.routing.mapper.SubCategoryMapper;
import java.util.List;
import java.util.stream.Collectors;

Expand All @@ -28,9 +26,7 @@
public class FilterAdvertisementService {
private final AdvertisementRepository advertisementRepository;
private final AdvertisementFilterMapper advertisementFilterMapper;
private final SubCategoryMapper subCategoryMapper;
private final AdvertisementSearchSpecificationFactory filterAdSpecFactory;
private final AdvertisementDistinctTopCategorySpecificationFactory distinctTopCategorySpecFactory;
private final SortStrategyFactory sortStrategyFactory;
private final FilterValidator filterValidator;
private final FilterPriceRangeService filterPriceRangeService;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package ua.everybuy.service.advertisement.search;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.springframework.stereotype.Service;
import ua.everybuy.database.entity.Advertisement;
import ua.everybuy.database.entity.AdvertisementDocument;
import ua.everybuy.database.repository.advertisement.AdvertisementRepository;
import ua.everybuy.routing.mapper.AdvertisementDocumentMapper;
import java.io.IOException;
import java.util.List;

@Service
@RequiredArgsConstructor
public class ElasticsearchReindexService {
private final AdvertisementRepository advertisementRepository;
private final AdvertisementDocumentMapper mapper;
private final RestHighLevelClient restHighLevelClient;
private final ObjectMapper objectMapper;

private static final String INDEX_NAME = "advertisements";

public String reindexAllAdvertisements() throws IOException {
createIndexIfNotExists();
deleteAllDocuments();

List<Advertisement> allAds = advertisementRepository.findAll();
BulkRequest bulkRequest = createBulkIndexRequest(allAds);

restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
return "Successfully reindexed %d advertisements".formatted(allAds.size());
}

private void createIndexIfNotExists() throws IOException {
if (!indexExists()) {
CreateIndexRequest request = new CreateIndexRequest(INDEX_NAME);
request.source(getIndexMapping(), XContentType.JSON);
restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
}
}

private boolean indexExists() throws IOException {
GetIndexRequest getIndexRequest = new GetIndexRequest(INDEX_NAME);
return restHighLevelClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
}

private void deleteAllDocuments() throws IOException {
DeleteByQueryRequest deleteRequest = new DeleteByQueryRequest(INDEX_NAME);
deleteRequest.setQuery(QueryBuilders.matchAllQuery());
deleteRequest.setConflicts("proceed");
restHighLevelClient.deleteByQuery(deleteRequest, RequestOptions.DEFAULT);
}

private BulkRequest createBulkIndexRequest(List<Advertisement> advertisements) throws JsonProcessingException {
BulkRequest bulkRequest = new BulkRequest();

for (Advertisement ad : advertisements) {
AdvertisementDocument doc = mapper.mapToDocument(ad);
doc.setId(ad.getId());

IndexRequest indexRequest = new IndexRequest(INDEX_NAME)
.id(String.valueOf(doc.getId()))
.source(objectMapper.writeValueAsString(doc), XContentType.JSON);

bulkRequest.add(indexRequest);
}

return bulkRequest;
}

private String getIndexMapping() {
return """
{
"settings": {
"analysis": {
"analyzer": {
"ukrainian": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"ukrainian_stop",
"ukrainian_stemmer"
]
}
},
"filter": {
"ukrainian_stop": {
"type": "stop",
"stopwords": "_ukrainian_"
},
"ukrainian_stemmer": {
"type": "stemmer",
"language": "ukrainian"
}
}
}
},
"mappings": {
"properties": {
"id": { "type": "long" },
"title": {
"type": "text",
"analyzer": "ukrainian",
"fields": {
"exact": { "type": "keyword" }
}
},
"description": { "type": "text" },
"price": { "type": "double" },
"creationDate": { "type": "date" },
"updateDate": { "type": "date" },
"isEnabled": { "type": "boolean" },
"userId": { "type": "long" },
"mainPhotoUrl": { "type": "text" },
"cityId": { "type": "long" },
"topSubCategoryId": { "type": "long" },
"lowSubCategoryId": { "type": "long" },
"productType": { "type": "keyword" },
"section": { "type": "keyword" }
}
}
}
""";
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
package ua.everybuy.service.advertisement.search.filters;

import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.common.unit.Fuzziness;
import org.springframework.stereotype.Component;

@Component
public class KeywordFilter implements SearchFilter<String> {

@Override
public void apply(BoolQueryBuilder query, String keyword) {
if (keyword != null && !keyword.isBlank()) {
query.should(QueryBuilders.matchQuery("title", keyword).fuzziness(Fuzziness.AUTO));
query.should(QueryBuilders.prefixQuery("title", keyword.toLowerCase()));
keyword = keyword.toLowerCase();
query.should(QueryBuilders.termQuery("title.exact", keyword).boost(5.0f));

if (keyword.matches(".*\\d+.*")) {
query.should(QueryBuilders.matchQuery("title", keyword)
.analyzer("ukrainian")
.operator(Operator.AND)
.boost(4.0f));
} else {
query.should(QueryBuilders.matchQuery("title", keyword)
.analyzer("ukrainian")
.fuzziness(Fuzziness.ONE)
.boost(3.0f));

query.should(QueryBuilders.matchPhraseQuery("title", keyword).boost(4.0f));
}

query.minimumShouldMatch(1);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

@Service
@RequiredArgsConstructor
public class AdvertisementSubCategoryService {
public class SubCategoryService {
private final TopLevelSubCategoryService topSubCategoryService;
private final LowLevelSubCategoryService lowSubCategoryService;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ua.everybuy.service.advertisement.delivery;
package ua.everybuy.service.delivery;

import org.springframework.stereotype.Component;

Expand Down
Loading
Loading