diff --git a/pom.xml b/pom.xml index 41d98ae3..cc220017 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.xceptance posters-demo-store - 2.3.0 + 2.4.0 jar Posters Demo Store @@ -265,6 +265,16 @@ jaxb-impl 2.3.2 + + org.apache.lucene + lucene-core + 9.1.0 + + + org.apache.lucene + lucene-analysis-common + 9.1.0 + diff --git a/src/main/java/conf/Module.java b/src/main/java/conf/Module.java index 8bf8f66d..a668c6cb 100644 --- a/src/main/java/conf/Module.java +++ b/src/main/java/conf/Module.java @@ -21,6 +21,8 @@ import com.google.inject.Inject; import controllers.JobController; +import models.LuceneSearch; +import models.SearchEngine; /** * Ninja uses Guice as injection tool. Define your bindings in this class, which you want to use via @{@link Inject} in @@ -44,5 +46,7 @@ protected void configure() bind(JobController.class); // bind scheduler class bind(Scheduler.class); + // bind search engine + bind(SearchEngine.class).to(LuceneSearch.class).asEagerSingleton(); } } diff --git a/src/main/java/conf/application.conf b/src/main/java/conf/application.conf index 7d7ab46e..fb756581 100644 --- a/src/main/java/conf/application.conf +++ b/src/main/java/conf/application.conf @@ -118,7 +118,7 @@ ebean.datasource.heartbeatsql = select 1 application.name = demo poster store -application.version = 2.3.0 +application.version = 2.4.0 application.cookie.prefix = NINJA diff --git a/src/main/java/conf/default-create.sql b/src/main/java/conf/default-create.sql index 2f16650d..5ce47bfe 100644 --- a/src/main/java/conf/default-create.sql +++ b/src/main/java/conf/default-create.sql @@ -161,15 +161,6 @@ create table topcategory ( create index ix_billingaddress_customer_id on billingaddress (customer_id); alter table billingaddress add constraint fk_billingaddress_customer_id foreign key (customer_id) references customer (id) on delete restrict on update restrict; -create index ix_cart_shipping_address_id on cart (shipping_address_id); -alter table cart add constraint fk_cart_shipping_address_id foreign key (shipping_address_id) references shippingaddress (id) on delete restrict on update restrict; - -create index ix_cart_billing_address_id on cart (billing_address_id); -alter table cart add constraint fk_cart_billing_address_id foreign key (billing_address_id) references billingaddress (id) on delete restrict on update restrict; - -create index ix_cart_credit_card_id on cart (credit_card_id); -alter table cart add constraint fk_cart_credit_card_id foreign key (credit_card_id) references creditcard (id) on delete restrict on update restrict; - create index ix_cartproduct_product_id on cartproduct (product_id); alter table cartproduct add constraint fk_cartproduct_product_id foreign key (product_id) references product (id) on delete restrict on update restrict; diff --git a/src/main/java/conf/default-drop.sql b/src/main/java/conf/default-drop.sql index 1ce98d4b..bfa40e25 100644 --- a/src/main/java/conf/default-drop.sql +++ b/src/main/java/conf/default-drop.sql @@ -1,8 +1,6 @@ alter table billingaddress drop constraint if exists fk_billingaddress_customer_id; drop index if exists ix_billingaddress_customer_id; -alter table cart drop constraint if exists fk_cart_customer_id; - alter table cart drop constraint if exists fk_cart_shipping_address_id; drop index if exists ix_cart_shipping_address_id; diff --git a/src/main/java/controllers/CatalogController.java b/src/main/java/controllers/CatalogController.java index edf3862d..0f2844f8 100644 --- a/src/main/java/controllers/CatalogController.java +++ b/src/main/java/controllers/CatalogController.java @@ -63,8 +63,22 @@ public Result productDetail(@Param("productId") final int productId, final Conte { final Map data = new HashMap(); WebShopController.setCommonData(data, context, xcpConf); + + //Use the ID to acquire corresponding product + Product obtainedProduct = Product.getProductById(productId); + + // Check if productId acquired a product + if (obtainedProduct !=null) + { + //Logger.warn("Product with ID {} not found", productId); + data.put("productDetail", Product.getProductById(productId)); + } + else + { + // Handle missing productId + return Results.redirect(xcpConf.NOT_FOUND_404); + } // put product to data map - data.put("productDetail", Product.getProductById(productId)); return Results.html().render(data); } diff --git a/src/main/java/controllers/SearchController.java b/src/main/java/controllers/SearchController.java index a79f6425..e2f19575 100644 --- a/src/main/java/controllers/SearchController.java +++ b/src/main/java/controllers/SearchController.java @@ -30,6 +30,7 @@ import conf.PosterConstants; import filters.SessionCustomerExistFilter; import models.Product; +import models.SearchEngine; import models.TopCategory; import ninja.Context; import ninja.FilterWith; @@ -53,6 +54,9 @@ public class SearchController @Inject PosterConstants xcpConf; + @Inject + SearchEngine searcher; + private final Optional language = Optional.of("en"); /** @@ -131,24 +135,18 @@ public Result getProductOfSearch(@Param("searchText") final String searchText, @ * @return A list of products that match the search text. */ private List searchForProducts(final String searchText, final int pageNumber, final Map data) { - // Divide search text by spaces - final String[] searchTerms = searchText.split(" "); - - // Create the query - final Query query = DB.find(Product.class); - - // Add search conditions for each term - for (String term : searchTerms) { - String likePattern = "%" + term.toLowerCase() + "%"; - query.where() - .or() - .ilike("descriptionDetail", likePattern) - .ilike("name", likePattern) - .endOr(); + // Search products with search engine, second param is the limit for returned results + List resultIds = searcher.search(searchText, 20); + + if (resultIds.isEmpty()) { + return List.of(); } - - // Log the generated query - System.out.println("Generated query: " + query.getGeneratedSql()); + else{ + // Create the query + final Query query = DB.find(Product.class); + + // Add search conditions + query.where().idIn(resultIds); final int pageSize = xcpConf.PRODUCTS_PER_PAGE; @@ -186,6 +184,7 @@ private List searchForProducts(final String searchText, final int pageN data.put("currentPage", pageNumber); return products; + } } @FilterWith(SessionCustomerExistFilter.class) diff --git a/src/main/java/models/Cart.java b/src/main/java/models/Cart.java index 4c0a6c06..2d80b910 100644 --- a/src/main/java/models/Cart.java +++ b/src/main/java/models/Cart.java @@ -58,19 +58,22 @@ public class Cart /** * The {@link ShippingAddress} of the order. */ - @ManyToOne + @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH}) + @DbForeignKey(noConstraint = true) private ShippingAddress shippingAddress; /** * The {@link BillingAddress} of the order. */ - @ManyToOne + @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH}) + @DbForeignKey(noConstraint = true) private BillingAddress billingAddress; /** * The {@link CreditCard}, the order is paid with. */ - @ManyToOne + @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH}) + @DbForeignKey(noConstraint = true) private CreditCard creditCard; /** diff --git a/src/main/java/models/LuceneSearch.java b/src/main/java/models/LuceneSearch.java new file mode 100644 index 00000000..a7164ff3 --- /dev/null +++ b/src/main/java/models/LuceneSearch.java @@ -0,0 +1,119 @@ +package models; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field.Store; +import org.apache.lucene.document.StoredField; +import org.apache.lucene.document.TextField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.store.ByteBuffersDirectory; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.QueryBuilder; + +import io.ebean.Ebean; +import ninja.lifecycle.Start; +import util.standalone.StemmingAnalyzer; + +public class LuceneSearch implements SearchEngine { + // setup analyzer + Analyzer prodAna; + // setup directory - for now in memory, alternative Option might be desirable + final Directory prodIndex = new ByteBuffersDirectory(); + + // standard constructor that sets up used analyzer as a standard analyzer + public LuceneSearch() { + prodAna = new StemmingAnalyzer(); + } + + @Start(order = 100) + public void firstIndexing() { + setup(); + } + + @Override + public void setup() { + indexData(); + } + + @Override + public List search(String searchText, int maxNumberOfHits) { + List results = new ArrayList(); + // setup query builder + QueryBuilder builder = new QueryBuilder(prodAna); + // setup queries + BooleanQuery.Builder fullQuery = new BooleanQuery.Builder(); + // check names + Query queryN = builder.createBooleanQuery("name", searchText, BooleanClause.Occur.MUST); + fullQuery.add(queryN, BooleanClause.Occur.SHOULD); + // check short description + Query querySD = builder.createBooleanQuery("overview", searchText, BooleanClause.Occur.MUST); + fullQuery.add(querySD, BooleanClause.Occur.SHOULD); + // check long description + Query queryLD = builder.createBooleanQuery("description", searchText, BooleanClause.Occur.MUST); + fullQuery.add(queryLD, BooleanClause.Occur.SHOULD); + // setup index reader and searcher and perform search + try { + // setup + IndexReader reader = DirectoryReader.open(prodIndex); + IndexSearcher searcher = new IndexSearcher(reader); + // search + TopDocs topDocs = searcher.search(fullQuery.build(), maxNumberOfHits); + ScoreDoc[] hits = topDocs.scoreDocs; + for (ScoreDoc scoreDoc : hits) { + results.add(Integer.parseInt(searcher.doc(scoreDoc.doc).get("id"))); + } + } catch (Exception e) { + // TODO: handle exception + e.printStackTrace(); + } + + return results; + } + + // currently indexes only products + private void indexData() { + + // setup index writer + config + IndexWriterConfig config = new IndexWriterConfig(prodAna); + try { + IndexWriter wri = new IndexWriter(prodIndex, config); + // loop through all products to add to the index + List products = getAllProducts(); + for (Product product : products) { + // create a 'document' (= an indexing target) + Document prodDoc = new Document(); + // Setup the fields in that document, we store the id so we can use it to retrieve search results + StoredField prodId = new StoredField("id", product.getId()); + TextField prodName = new TextField("name", product.getName(), Store.NO); + TextField prodShortDesc = new TextField("overview", product.getDescriptionOverview(), Store.NO); + TextField prodLongDesc = new TextField("description", product.getDescriptionDetail(), Store.NO); + prodDoc.add(prodId); + prodDoc.add(prodName); + prodDoc.add(prodShortDesc); + prodDoc.add(prodLongDesc); + // add the document with the products information to the index writer + wri.addDocument(prodDoc); + } + wri.close(); + } catch (Exception e) { + // TODO: handle exception + e.printStackTrace(); + } + } + + private List getAllProducts() { + return Ebean.find(Product.class).findList(); + } +} diff --git a/src/main/java/models/Order.java b/src/main/java/models/Order.java index 5778fa75..445b905c 100644 --- a/src/main/java/models/Order.java +++ b/src/main/java/models/Order.java @@ -688,8 +688,8 @@ public static void deleteOldPendingOrders() { // Get all orders that are pending and have creation time more than one day ago from the database irrespective of customer. // Delete those orders. - Ebean.delete(Ebean.find(Order.class) - .where().eq("orderStatus", "Pending").lt("lastUpdate", oneDayAgo).findList()); + Ebean.find(Order.class) + .where().eq("orderStatus", "Pending").lt("lastUpdate", oneDayAgo).delete(); } } \ No newline at end of file diff --git a/src/main/java/models/SearchEngine.java b/src/main/java/models/SearchEngine.java new file mode 100644 index 00000000..480d9c5a --- /dev/null +++ b/src/main/java/models/SearchEngine.java @@ -0,0 +1,8 @@ +package models; + +import java.util.List; + +public interface SearchEngine { + void setup(); + List search(String searchText, int maxNumberOfHits); +} diff --git a/src/main/java/util/standalone/StemmingAnalyzer.java b/src/main/java/util/standalone/StemmingAnalyzer.java new file mode 100644 index 00000000..8f30aed4 --- /dev/null +++ b/src/main/java/util/standalone/StemmingAnalyzer.java @@ -0,0 +1,14 @@ +package util.standalone; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.LowerCaseFilter; +import org.apache.lucene.analysis.en.PorterStemFilter; +import org.apache.lucene.analysis.standard.StandardTokenizer; +import org.apache.lucene.analysis.Tokenizer; + +public class StemmingAnalyzer extends Analyzer { + @Override + protected TokenStreamComponents createComponents(String fieldName) { + Tokenizer source = new StandardTokenizer(); + return new TokenStreamComponents(source, new PorterStemFilter(new LowerCaseFilter(source))); + } +}