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)));
+ }
+}