Skip to content

Commit

Permalink
Merge pull request #459 from royllo/377-use-redis-for-the-search-engine
Browse files Browse the repository at this point in the history
377 use redis for the search engine
  • Loading branch information
straumat authored Jan 11, 2024
2 parents 127292c + 992b588 commit 3955898
Show file tree
Hide file tree
Showing 18 changed files with 341 additions and 178 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.Optional;
Expand Down Expand Up @@ -31,7 +32,20 @@ public interface AssetRepository extends JpaRepository<Asset, Long> {
Optional<Asset> findByAssetIdAlias(String assetIdAlias);

/**
* Find an asset by its complete or partial name.
* Find assets by its complete or partial name.
* Takes advantage of PostgreSQL "pg_trgm" and "ILIKE".
*
* @param searchTerm search term
* @param pageable pagination
* @return results
*/
@Query(value = "SELECT * FROM ASSET WHERE NAME ILIKE %?1%",
countQuery = "SELECT count(*) FROM ASSET WHERE NAME ILIKE %?1%",
nativeQuery = true)
Page<Asset> findByName(String searchTerm, Pageable pageable);

/**
* Find assets by its complete or partial name.
*
* @param name complete or partial name
* @param pageable page parameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,6 @@
*/
public interface AssetService {

/**
* Query assets following this algorithm:
* - Search if the "query" parameter is a tweaked group keys (asset group) > returns all assets of this asset group.
* - Search if the "query" parameter is an assetId (asset) > returns the asset.
* - Else search the "query" parameter in assets names.
*
* @param query the query
* @param page the page we want to retrieve (First page is page 1)
* @param pageSize the page size
* @return list of assets corresponding to the search
*/
Page<AssetDTO> queryAssets(String query, int page, int pageSize);

/**
* Add an asset.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,15 @@
import org.royllo.explorer.core.service.bitcoin.BitcoinService;
import org.royllo.explorer.core.util.base.BaseService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;

import java.math.BigInteger;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.Optional;

import static java.util.stream.Collectors.joining;
import static org.royllo.explorer.core.util.constants.AnonymousUserConstants.ANONYMOUS_USER;
import static org.royllo.explorer.core.util.constants.TaprootAssetsConstants.ASSET_ID_SIZE;
import static org.royllo.explorer.core.util.constants.TaprootAssetsConstants.ASSET_ID_LENGTH;

/**
* {@link AssetService} implementation.
Expand All @@ -50,65 +47,6 @@ public class AssetServiceImplementation extends BaseService implements AssetServ
/** Content service. */
private final ContentService contentService;

@Override
public Page<AssetDTO> queryAssets(@NonNull final String query,
final int page,
final int pageSize) {
logger.info("Searching for {}", query);

// Checking constraints.
assert page >= 1 : "Page number starts at page 1";

// Results.
Page<AssetDTO> results = Page.empty();

// Search if the "query" parameter is a tweaked group key (asset group) > returns all assets of this asset group.
final Optional<AssetGroupDTO> assetGroup = assetGroupService.getAssetGroupByAssetGroupId(query);
if (assetGroup.isPresent()) {
results = getAssetsByAssetGroupId(assetGroup.get().getAssetGroupId(), page, pageSize);
}

// If nothing found, we search if there is this asset in database with this exact asset id.
if (results.isEmpty()) {
final Optional<Asset> assetIdSearch = assetRepository.findByAssetId(query);
if (assetIdSearch.isPresent()) {
results = new PageImpl<>(assetIdSearch.stream()
.map(ASSET_MAPPER::mapToAssetDTO)
.toList());
}
}

// If nothing found, we will search on asset id alias.
if (results.isEmpty()) {
final Optional<Asset> assetIdAliasSearch = assetRepository.findByAssetIdAlias(query);
if (assetIdAliasSearch.isPresent()) {
results = new PageImpl<>(assetIdAliasSearch.stream()
.map(ASSET_MAPPER::mapToAssetDTO)
.toList());
}
}

// If nothing found, we search if there is an asset with "query" parameter as complete or partial asset name.
if (results.isEmpty()) {
results = assetRepository.findByNameContainsIgnoreCaseOrderByName(query,
PageRequest.of(page - 1, pageSize)).map(ASSET_MAPPER::mapToAssetDTO);
}

// Displaying logs and return results.
if (results.isEmpty()) {
logger.info("For '{}', there is no results", query);
} else {
logger.info("For '{}', {} result(s) with assets id(s): {}",
query,
results.getTotalElements(),
results.stream()
.map(AssetDTO::getId)
.map(Objects::toString)
.collect(joining(", ")));
}
return results;
}

@Override
public AssetDTO addAsset(@NonNull final AssetDTO newAsset) {
logger.info("Adding asset {}", newAsset);
Expand Down Expand Up @@ -220,7 +158,7 @@ public Optional<AssetDTO> getAssetByAssetId(final String assetId) {
return Optional.empty();
}

if (assetId.length() == ASSET_ID_SIZE) {
if (assetId.length() == ASSET_ID_LENGTH) {
// We received an asset id (we know it because of the size).
Optional<Asset> asset = assetRepository.findByAssetId(assetId.trim());
if (asset.isPresent()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package org.royllo.explorer.core.service.search;

import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.royllo.explorer.core.domain.asset.Asset;
import org.royllo.explorer.core.dto.asset.AssetDTO;
import org.royllo.explorer.core.dto.asset.AssetGroupDTO;
import org.royllo.explorer.core.repository.asset.AssetRepository;
import org.royllo.explorer.core.service.asset.AssetGroupService;
import org.royllo.explorer.core.util.base.BaseService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;

import javax.sql.DataSource;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.Objects;
import java.util.Optional;

import static java.util.stream.Collectors.joining;
import static org.royllo.explorer.core.util.constants.TaprootAssetsConstants.ASSET_ALIAS_LENGTH;
import static org.royllo.explorer.core.util.constants.TaprootAssetsConstants.ASSET_ID_LENGTH;
import static org.royllo.explorer.core.util.constants.TaprootAssetsConstants.TWEAKED_GROUP_KEY_LENGTH;

/**
* {@link SearchService} SQL implementation.
*/
@Service
@RequiredArgsConstructor
@SuppressWarnings("checkstyle:DesignForExtension")
public class SQLSearchServiceImplementation extends BaseService implements SearchService {

/** Datasource. */
private final DataSource dataSource;

/** Assert repository. */
private final AssetRepository assetRepository;

/** Asset group service. */
private final AssetGroupService assetGroupService;

/** Indicates if it's a postgresql database. */
@Getter
private boolean isUsingPostgreSQL = false;

@PostConstruct
public void init() {
try {
DatabaseMetaData metaData = dataSource.getConnection().getMetaData();
String databaseProductName = metaData.getDatabaseProductName();
isUsingPostgreSQL = "PostgreSQL".equals(databaseProductName);
} catch (SQLException e) {
logger.error("Impossible to retrieve database metadata {}", e.getMessage());
}
}

@Override
public Page<AssetDTO> queryAssets(@NonNull final String query,
final int page,
final int pageSize) {
logger.info("Searching for {}", query);

// Checking constraints.
assert page >= 1 : "Page number starts at page 1";

// Cleaning the query.
final String cleanedQuery = query.trim();

// =============================================================================================================
// TWEAKED_GROUP_KEY_SIZE search.
if (cleanedQuery.length() == TWEAKED_GROUP_KEY_LENGTH) {
// Search if the "query" parameter is a tweaked group key (asset group) > returns all assets of this asset group.
final Optional<AssetGroupDTO> assetGroup = assetGroupService.getAssetGroupByAssetGroupId(cleanedQuery);
if (assetGroup.isPresent()) {
logger.info("The query '{}' corresponds to a tweaked group key", cleanedQuery);
return assetRepository.findByAssetGroup_AssetGroupId(assetGroup.get().getAssetGroupId(), PageRequest.of(page - 1, pageSize))
.map(ASSET_MAPPER::mapToAssetDTO);
}
}

// =============================================================================================================
// ASSET_ID search.
if (cleanedQuery.length() == ASSET_ID_LENGTH) {
// Search if the "query" parameter is an asset id.
final Optional<Asset> assetIdSearch = assetRepository.findByAssetId(cleanedQuery);
if (assetIdSearch.isPresent()) {
logger.info("The query '{}' corresponds to an asset id", cleanedQuery);
return new PageImpl<>(assetIdSearch.stream()
.map(ASSET_MAPPER::mapToAssetDTO)
.toList());
}
}

// =============================================================================================================
// ASSET_ID_ALIAS search.
if (cleanedQuery.length() == ASSET_ALIAS_LENGTH) {
// If nothing found, we will search on asset id alias.
final Optional<Asset> assetIdAliasSearch = assetRepository.findByAssetIdAlias(cleanedQuery);
if (assetIdAliasSearch.isPresent()) {
logger.info("The query '{}' corresponds to an asset id alias", cleanedQuery);
return new PageImpl<>(assetIdAliasSearch.stream()
.map(ASSET_MAPPER::mapToAssetDTO)
.toList());
}
}

// If nothing found, we search if there is an asset with "query" parameter as complete or partial asset name.
Page<AssetDTO> results;
if (isUsingPostgreSQL) {
// PostgreSQL "ILIKE" search.
results = assetRepository.findByName(cleanedQuery,
PageRequest.of(page - 1, pageSize)).map(ASSET_MAPPER::mapToAssetDTO);
} else {
results = assetRepository.findByNameContainsIgnoreCaseOrderByName(cleanedQuery,
PageRequest.of(page - 1, pageSize)).map(ASSET_MAPPER::mapToAssetDTO);
}

// Displaying logs and return results.
if (results.isEmpty()) {
logger.info("For '{}', there is no results", cleanedQuery);
} else {
logger.info("For '{}', {} result(s) with assets id(s): {}",
cleanedQuery,
results.getTotalElements(),
results.stream()
.map(AssetDTO::getId)
.map(Objects::toString)
.collect(joining(", ")));
}

return results;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.royllo.explorer.core.service.search;

import org.royllo.explorer.core.dto.asset.AssetDTO;
import org.springframework.data.domain.Page;

/**
* Search service.
*/
public interface SearchService {

/**
* Query assets following this algorithm:
* - Search if the "query" parameter is a tweaked group keys (asset group) > returns all assets of this asset group.
* - Search if the "query" parameter is an assetId (asset) > returns the asset.
* - Else search the "query" parameter in assets names.
*
* @param query the query
* @param page the page we want to retrieve (First page is page 1)
* @param pageSize the page size
* @return list of assets corresponding to the search
*/
Page<AssetDTO> queryAssets(String query, int page, int pageSize);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* Search related services.
*/
package org.royllo.explorer.core.service.search;
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
public class TaprootAssetsConstants {

/** Tweaked group key size is always 33 bytes (66 characters) and hexadecimal. */
public static final int TWEAKED_GROUP_KEY_SIZE = 66;
public static final int TWEAKED_GROUP_KEY_LENGTH = 66;

/** Asset id size is always 32 bytes (64 characters) and hexadecimal. */
public static final int ASSET_ID_SIZE = 64;
public static final int ASSET_ID_LENGTH = 64;

/** The length of the asset id alias. */
public static final int ASSET_ALIAS_LENGTH = 8;

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import java.util.Random;

import static org.royllo.explorer.core.util.constants.TaprootAssetsConstants.ASSET_ALIAS_LENGTH;

/**
* Asset mapper decorator.
* This is used to calculate the asset id alias if not set.
Expand All @@ -17,9 +19,6 @@ public abstract class AssetMapperDecorator implements AssetMapper {
/** Characters to choose from when generating an asset id alias. */
private static final String CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

/** The length of the asset id alias. */
public static final int ALIAS_LENGTH = 8;

/** Random number generator. */
private final Random random = new Random();

Expand Down Expand Up @@ -61,12 +60,13 @@ public final AssetDTO mapToAssetDTO(final DecodedProofResponse.DecodedProof sour

/**
* Returns a random alias.
*
* @return random alias
*/
private String getAssetIdAlias() {
StringBuilder assetIdAlias = new StringBuilder(ALIAS_LENGTH);
StringBuilder assetIdAlias = new StringBuilder(ASSET_ALIAS_LENGTH);

for (int i = 0; i < ALIAS_LENGTH; i++) {
for (int i = 0; i < ASSET_ALIAS_LENGTH; i++) {
int index = random.nextInt(CHARACTERS.length());
assetIdAlias.append(CHARACTERS.charAt(index));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.4.xsd">
<changeSet author="straumat" id="1.0.0-postgresql" dbms="postgresql" failOnError="false">

<!-- LTree extension installation -->
<sql dbms="postgresql">create extension ltree;</sql>
<!-- Extension installation -->
<sql dbms="postgresql">create extension if not exists pg_trgm;</sql>

<!-- Add a GIN index on ASSET.NAME -->
<sql dbms="postgresql">
CREATE INDEX INDEX_ASSET_GIN_NAME ON ASSET USING GIN (NAME gin_trgm_ops);
</sql>

</changeSet>
</databaseChangeLog>
Loading

0 comments on commit 3955898

Please sign in to comment.