在Google Java App Engine上實現(xiàn)文檔存儲和搜索
原創(chuàng)【51CTO技術(shù)譯文】為什么Java程序員要考慮使用Google的Java App Engine呢,主要有以下幾點原因:只要你的頁面訪問量每月不超過500萬,Google就免費向你提供空間。如果訪問量超過了這一限額,你也可以隨時通過升級為付費用戶取消這一限制。
◆Google的App Engine 平臺(包括Java和Python版本)讓你不用做什么額外工作就有很強的伸縮性
◆App Engine 提供了一個功能很強的管理界面,你可以通過它查看錯誤日志,瀏覽你所保存的數(shù)據(jù),分析程序的性能(例如請求響應(yīng)時間等),還可以實時監(jiān)控你所部署的應(yīng)用。即便是和Amazon的EC2這樣優(yōu)秀的Web控制臺比起來,Google的Web應(yīng)用程序管理功能也毫不遜色。
◆只要你愿意,你也可以通過App Engine SDK 把App Engine 上的應(yīng)用遷移到你自己的服務(wù)器上,當(dāng)然,這樣就會損失一些伸縮性(scalability)了。
◆因為在App Engine上開發(fā)程序時使用的都是標準的 API,所以當(dāng)你要把應(yīng)用移植部署到其它平臺上時,就只需要對程序作非常小的改動了。不過反過來做就不是這么簡單了。比如說如果你的程序調(diào)用大量的J2EE API函數(shù),或者說依賴于關(guān)系型數(shù)據(jù)庫等等,那么把這些程序移植到App Engine上就非常麻煩。
◆那些用J2EE寫Web程序的開發(fā)者們可能一開始會覺得App Engine 的種種限制讓人覺得很不適應(yīng),但是這樣做的好處也是很明顯的,服務(wù)器的花費將大大減少。如果你想要更大的自由度和伸縮性,那么你還可以考慮Amazon的EC2服務(wù)(我是既用App Engine,也用EC2)。
本文接下來將介紹Java開發(fā)者如何使用Google應(yīng)用程序引擎。它演示了如何在App Engine上編寫實現(xiàn)文檔的存儲和搜索功能。本文還探討了Java App Engine文檔里的一些有用技術(shù)和應(yīng)用程序示例。
你需要作的準備
◆Eclipse或IntelliJ IDEA開發(fā)環(huán)境
◆一個App Engine 帳號,如果還沒有的話,在這里申請(沒有App Engine 帳號的開發(fā)者可以通過在你自己電腦上安裝App Engine SDK體驗它)
◆下載App Engine SDK 供本地開發(fā)時使用
◆安裝Eclipse的或IntelliJ 的Java App Engine 插件。  
 
示例工程里的文件
圖1 示例工程里的文件
許多Java開發(fā)人員使用 Lucene (或基于Lucene的框架)來實現(xiàn)搜索功能。但是,在App Engine環(huán)境下使用Lucene的內(nèi)存索引模式?jīng)]有什么好處。我們的這個示例工程另辟蹊徑在App Engine平臺上實現(xiàn)了搜索功能。
App Engine的持久性數(shù)據(jù)存儲效率是非常高的,但它不使用關(guān)系模型,也沒有Hibernate這樣的對象關(guān)系映射(Object Relational Mapping ,ORM)框架。不過,App Engine還是提供了對一些標準的持久性API,如JDO,JPA,以及JCache。我們的示例程序使用JDO實現(xiàn)數(shù)據(jù)持久(data persistence)。
這個程序部署在這里。每個使用這個演示程序的人都可以把數(shù)據(jù)清空從頭再來,所以你這次添加的信息下次可以就會看不到了。
作者注:這個程序演示了JDO的使用以及如何用JDO實現(xiàn)搜索,為了突出重點,程序沒有增加對多用戶這些功能的支持。
圖1顯示了這個Java App Engine項目所包含的文件。后續(xù)的章節(jié)將詳細介紹packagecom.kbsportal.model 里的模型類和 com.kbsportal.persistence 里的持久類PMF。由于packagecom.kbsportal.util這個包里的各種類和App Engine里的差別較大,我們就不在這里作過多討論了。如果要詳細了解這些,你可以看看我們的源代碼以及JSP文件(在 war/WEB-INF目錄里)。我們也會對JSP文件里某些Java代碼片段加以解釋。
使用JDO實現(xiàn)數(shù)據(jù)持久化
JDO是一個用于持久化Java對象的古老API。起初,為了實現(xiàn)持久化存儲,JDO要求開發(fā)者必須編寫和維護XML文件,以提供Java類的數(shù)據(jù)映射屬性。Google使用 DataNucleus 工具自動完成這一過程。你只需要在你的Java模型類里面加以注解,DataNucleus工具就會自動為你維護正確的數(shù)據(jù)映射關(guān)系。如果使用了Eclipse的或IntelliJ IDEA的App Engine插件,當(dāng)你編寫持久類時,DataNucleus工具就會自動在后臺作用。
警告:JDO和App Engine放到一起有時候會產(chǎn)生兼容性問題。如果你是在本地用Eclipse開發(fā),只要刪除目錄 WEBAPP /war/WEB-INF/ appengine-generated/ local_db.bin里的文件。 如果你的Web應(yīng)用已經(jīng)部署上去了而且要修改模型類,那么你只需在App Engine控制臺中把已有的索引文件刪除即可
以下各節(jié)將介紹兩個持久類的實現(xiàn)并探討這些基于JDO實現(xiàn)的代碼。
#p#
文檔模型類
Eclipse或IntelliJ IDEA的App Engine插件與JDO以及DataNucleus工具的組合非常好用。使用這個組合設(shè)計和實現(xiàn)你自己的模型文件,并添加必須的注解,這些對你來說應(yīng)該不成問題。不過你還是要注意DataNucleus工具在后臺運行時所提示的錯誤信息。
在開始設(shè)計實現(xiàn)自己的持久類前,不妨先看看下面這個模型類,它是用來反映一個文件模型的。這個類在定義時會引入所需的JDO 類(實際上你的編輯器會自動幫你填寫這些包含語句)。第一行注釋聲明了這個類是持久的。這個類被標識為APPLICATION,這樣你就可以為那些創(chuàng)建后就將持久存在的對象分配ID。如果你要為數(shù)據(jù)存儲對象分配ID,那么你可以把類型指定為DATASTORE。
- package com.kbsportal.model;
 - import javax.jdo.annotations.IdentityType;
 - import javax.jdo.annotations.PersistenceCapable;
 - import javax.jdo.annotations.Persistent;
 - import javax.jdo.annotations.PrimaryKey;
 - @PersistenceCapable(identityType=IdentityType.APPLICATION)
 - public class Document {
 
這段代碼聲明了把成員變量uri作為在數(shù)據(jù)存儲里查找Document對象時的主鍵。JDO的索引主鍵也被設(shè)為URI。本文的示例文本存儲在IndexToken這個類里面使用了這個主鍵(IndexToken類將在下一節(jié)進一步討論)。這段代碼還特別說明了title, content以及numWords這幾個成員變量要持久保存。
- @PrimaryKey private String uri;
 - @Persistent private String title;
 - @Persistent private String content;
 - @Persistent private int numWords;
 
類聲明里的其它部分則不包含JDO具體說明。
- public Document(String uri, String title, String content) {
 - super();
 - setContent(content);
 - this.title = title;
 - this.key = uri;
 - }
 - public String getUri() { return key; }
 - public String getTitle() { return title; }
 - public void setTitle(String title) { this.title = title; }
 - public String getContent() { return content; }
 - public void setContent(String content) {
 - this.content = content;
 - this.numWords = content.split("[\\ \\.\\,\\:\\;!]").length;
 - System.out.println("** numWords = " + numWords + " content: "+content);
 - }
 - public int getNumWords() { return numWords; }
 - }
 
注意在內(nèi)容字符串上所作的長度限制;GoogleApp Engine的數(shù)據(jù)存儲限制字符串不得超過500個字符。(使用com.google.appengine.api.datastore.Textfors可以獲得沒有長度限制的字串。 )
#p#
IndexToken模型類
該IndexToken類基于JDO實現(xiàn)了搜索功能。這個類有兩種工作模式:整詞索引、整詞及詞前綴索引。在源文件的頭部你可以通過一個常量指定它的工作模式:
- package com.kbsportal.model;
 - import java.util.ArrayList;
 - import java.util.Collections;
 - import java.util.Comparator;
 - import java.util.HashMap;
 - import java.util.List;
 - import javax.jdo.PersistenceManager;
 - import javax.jdo.annotations.IdGeneratorStrategy;
 - import javax.jdo.annotations.IdentityType;
 - import javax.jdo.annotations.Index;
 - import javax.jdo.annotations.PersistenceCapable;
 - import javax.jdo.annotations.Persistent;
 - import javax.jdo.annotations.PrimaryKey;
 - import com.kbsportal.persistence.PMF;
 - import com.kbsportal.util.NoiseWords;
 - import com.kbsportal.util.Pair;
 - import com.kbsportal.util.SearchResult;
 - @PersistenceCapable(identityType=IdentityType.APPLICATION)
 - public class IndexToken {
 - static boolean MATCH_PARTIAL_WORDS = true; // package visibility
 
把這個標志設(shè)置為true,就會開啟單詞的前綴匹配功能,類似于搜索關(guān)鍵字自動校正功能。
現(xiàn)在我們該看看如何建立索引片段(可能還包括單詞前綴的索引片段)以及如何確定每個索引片段的匹配度。以下是具體的代碼(來自IndexToken.java包里的源文件,它是作為一個單獨的局部類實現(xiàn)的,以方便在其他項目重復(fù)使用) :
- class StringPrefix {
 - public List getPrefixes(String str) {
 - List ret = new ArrayList();
 - String[] toks = str.toLowerCase().split("[\\ \\.\\,\\:\\;\\(\\)\\-\\[\\]!]");
 - for (String s : toks) {
 - if (!(NoiseWords.checkFor(s))) {
 - if (!IndexToken.MATCH_PARTIAL_WORDS) { // exact words only
 - ret.add(new Pair(s, 1f));
 - } else { // or, also match word prefixes
 - int len = s.length();
 - if (len > 2) {
 - ret.add(new Pair(s, 1f));
 - if (len > 3) {
 - int start_index = 1 + (len / 2);
 - for (int i = start_index; i < len; i++) {
 - ret.add(new Pair(s.substring(0, i), (0.25f * (float) i) / (float) len));
 - }
 - }
 - }
 - }
 - }
 - }
 - return ret;
 - }
 - }
 
應(yīng)用中的一些理念
通過使用 Peter Norvig的拼寫檢查算法可以實現(xiàn)更完整的拼寫檢查功能。使用相對較低的相關(guān)系數(shù)可以生成錯誤的拼寫序列和IndexToken實例。在我所寫的書"Practical Artificial Intelligence Programming in Java"的第9章里有一個Java版本的 Norvig算法實現(xiàn)。
#p#
其它實現(xiàn)方法
我在另一個大項目里使用了這些代碼,那個項目需要一個彈出式的文字補全提示;我們存儲的這些前綴起到了“雙重作用”。本文主要講解基于JDO的文件存儲和搜索,但你可以簡單地使用一個JavaScript庫,例如 Prototype或GWT實現(xiàn)彈出的提示菜單。另外,你也可以只把詞干作為 IndexToken實例保存。點擊此處查看相關(guān)Java詞根提取程序。
 
Pair這個類是在com.kbsportal.util包里實現(xiàn)的,這個包里面還有另外兩個類: NoiseWords和SearchResults 。我們在此不再追究這些類的細節(jié)。今后我們將深入這些源文件。
要完成IndexToken,以及示例程序的其余部分,我們要用到JDO的API,首先是在類屬性說明里加入這些注解:
- @PrimaryKey
 - @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
 - private Long id;
 - @Persistent @Index private String textToken;
 - @Persistent private String documentUri;
 - @Persistent private Float ranking;
 
@Persistent 標示這個成員在整個對象被保存時要被插入到數(shù)據(jù)存儲里去。valueStrategy的值是可選的,按上面這樣設(shè)置是表明你希望數(shù)據(jù)存儲為你這個類的ID屬性自動賦值。@PrimaryKey 注釋讓DataNucleus工具知道,在查找數(shù)據(jù)存儲區(qū)里的這種對象時要以該參數(shù)為主鍵。
作者注:通常情況下都是通過主鍵獲取對象。然而,在我們這個程序里,我們將要通過IndexToken類的參數(shù)值 textToken 來查找對象。但是我們不能使用參數(shù)textToken 作為主鍵,因為這樣有可能導(dǎo)致在數(shù)據(jù)存儲區(qū)里有主鍵一樣的不同實例出現(xiàn)。
下面這個成員方法能獲取文件ID(文件的URI)以及文件中的一段文字,實例化一個IndexToken類:
- public static void indexString(String document_id, String text) {
 - PersistenceManager pm = PMF.get().getPersistenceManager();
 - List lp = new StringPrefix().getPrefixes(text);
 - for (Pair p : lp) {
 - if (p.str.length() > 0 && !Character.isDigit(p.str.charAt(0))) {
 - pm.makePersistent(new IndexToken(document_id, p.str, p.f));
 - }
 - }
 - }
 
這段代碼用到了StringPrefix 類。另外還使用了工具類PMF(等下我們就會更詳細地去了解它)來獲得一個App Engine持久管理器(persistence manager)的實例。這類似于一個JDBC 連接對象。
在IndexToken里還有一個值得一提的地方就是search這個靜態(tài)方法.
- public static List search(String query) {
 - List< SearchResult> ret = new ArrayList< SearchResult>();
 - PersistenceManager pm = PMF.get().getPersistenceManager();
 - String [] tokens = query.toLowerCase().split(" ");
 - HashMap matches = new HashMap();
 
此方法返回SearchResult類的實例。查詢字符串被轉(zhuǎn)換為小寫并被分割。對于每一個片段,你都將再次用StringPrefix計算前綴(以及原始單詞) ,計算結(jié)果將用于查找包含這些關(guān)鍵詞的文件:
- for (String token : tokens) {
 - List lp = new StringPrefix().getPrefixes(token);
 - for (Pair p : lp) {
 - String q2 = "select from " + IndexToken.class.getName() + " where textToken == '" + p.str + "'";
 - @SuppressWarnings("unchecked")
 - List itoks = (List) pm.newQuery(q2).execute();
 
這個查詢字符串可能看起來會覺得有點像標準的SQL語句 ,但不是。其實它們是JDO的查詢語言( JDOQL ) 。它從一個在數(shù)據(jù)存儲區(qū)持久化了的類里面取數(shù)據(jù),而不是像SQL語句那樣通過一個數(shù)據(jù)庫的表名來提取數(shù)據(jù)。TextToken就是IndexToken 的一個持久化參數(shù)。這個JDOQL能返回數(shù)據(jù)存儲區(qū)中所有textToken成員參數(shù)與查詢關(guān)鍵字匹配的IndexToken實例。(51CTO編者注:JDOQL是JDO的查詢語言;它有點象SQL,但卻是依照Java的語法的。)
搜索功能的其它部分實現(xiàn)起來就沒有什么難點了。只需要保存所有的文件匹配以及根據(jù)匹配度計算出的排名權(quán)重。
- for (IndexToken it : itoks) {
 - Float f = matches.get(it.getDocumentUri());
 - if (f == null) f = 0f;
 - f += it.getRanking();
 - matches.put(it.getDocumentUri(), f);
 - }
 - }
 - }
 
這樣我們就建立好了查詢關(guān)鍵字與文件之間的映射關(guān)系,還知道了這些文件的URI以及排名權(quán)重。我們只需要把匹配結(jié)果從數(shù)據(jù)存儲區(qū)里取出來就可以了(只有這樣我們才有結(jié)果可顯示), 然后把這些與關(guān)鍵字相匹配的文檔按匹配度從高到低排列,就形成了搜索結(jié)果。
- for (String s : matches.keySet()) {
 - String q2 = "select from " + Document.class.getName() + " where uri == '" + s + "'";
 - @SuppressWarnings("unchecked")
 - List itoks = (List) pm.newQuery(q2).execute();
 - if (!itoks.isEmpty()) {
 - int num_words = itoks.get(0).getNumWords();
 - ret.add(new SearchResult(s, matches.get(s) / (float)(num_words), itoks.get(0).getTitle()));
 - }
 - }
 - Collections.sort(ret, new ValueComparator());
 - return ret;
 - }
 
ValueComparato這個類是在源文件IndexToken.java里定義的,作用就是對搜索結(jié)果進行排序。
- static class ValueComparator implements Comparator {
 - public int compare(SearchResult o1, SearchResult o2) {
 - return (int)((o2.score - o1.score) * 100);
 - }
 - }
 
處理持久性數(shù)據(jù)存儲:PMF類
我們這里所展示的PMF類代碼是從Google的文檔里復(fù)制過來的。這個類創(chuàng)建了一個私有的PersistenceManagerFactory實例并重用它。
- package com.kbsportal.persistence;
 - import javax.jdo.JDOHelper;
 - import javax.jdo.PersistenceManagerFactory;
 - public final class PMF {
 - private static final PersistenceManagerFactory pmfInstance =
 - JDOHelper.getPersistenceManagerFactory("transactions-optional");
 - private PMF() {}
 - public static PersistenceManagerFactory get() {
 - return pmfInstance;
 - }
 - }
 
#p#
示例程序的JSP頁面
在寫JSP頁面時,我通常最開始是把Java代碼嵌入到JSP頁面里,到最后,我再把一些公用代碼提取出來放到自定義的JSP標簽庫里,再給模型類添加上額外的行為。在這個程序里,我就不演示最后這幾步清理工作了。
作為首頁顯示的index.jsp頁面是用來顯示系統(tǒng)里所有的文件的。它也包含了一些可選的調(diào)試代碼(我通常會把這些調(diào)試代碼注釋掉),可以列出所有IndexToken類的實例(見 圖2 ) 。index.jsp 這個文件最開頭的部分引入了一些必要的類,定義了HTML頭信息,然后還引入了menu.jsp,這個文件是用來作分頁條的。
- < %@ page import="javax.jdo.*, java.util.*,
 - com.kbsportal.model.*,com.kbsportal.persistence.PMF" %>
 - < %@ page language="java" contentType="text/html; charset=ISO-8859-1"
 - pageEncoding="ISO-8859-1"%>
 - < !DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
 - "http://www.w3.org/TR/html4/loose.dtd">
 - < html>
 - < head>
 - < meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
 - < title>KBSportal Java App Engine Search Demo< /title>
 - < /head>
 - < body>
 - < %@ include file="menu.jsp" %>
 
圖2 列出所有文件:調(diào)試代碼列出了所有IndexToken 實例,并顯示了一些索引片段。
在IndexToken實例里我們已經(jīng)見過JDOQL查詢語句。在這里,查詢語句返回所有文件對象:
- < h2>All documents:< /h2>
 - < %
 - PersistenceManager pm = PMF.get().getPersistenceManager();
 - Query query = pm.newQuery(Document.class);
 - try {
 - List< Document> results = (List< Document>)
 - query.execute();
 - if (results.iterator().hasNext()) {
 - for (Document d : results) {
 - System.out.println("key: "+d.getUri() +
 - ", title: "+d.getTitle());
 - %>
 - < h3>< %=d.getTitle()%>< /h3>
 - < p>< %=d.getContent()%>< /p>
 - < %
 - }
 - }
 - } finally {
 - query.closeAll();
 - }
 - %>
 
這里我們沒有用JDOQL查詢語句,而是用了一個查詢對象來獲取數(shù)據(jù),這樣我們所獲得的查詢結(jié)果就在其它JSP文件里也可以使用了,如果你只想獲取某個特定標題的文件,那么通過下面的代碼可以篩選結(jié)果:
- String title_to_find = "Dogs and Cats"
 - query.setFilter("title == " + title_to_find);
 
index.jsp這個文件的后半部分也包含一些調(diào)試代碼,在調(diào)試Web程序時我們可能會需要啟用它。這段代碼與之前那段調(diào)試代碼幾乎完全一樣,只不過這段代碼顯示的是所有的IndexToken實例。
- query = pm.newQuery(IndexToken.class);
 - try {
 - List
 results = (List ) query.execute(); - if (results.iterator().hasNext()) {
 - for (IndexToken indexToken : results) {
 
圖3 用于向數(shù)據(jù)存儲區(qū)添加文件的表單:這個JSP頁面提供了一個可以向系統(tǒng)增加“文件” 的HTML輸入框
new_document.jsp這個文件提供了一個可以向系統(tǒng)增加“文件” 的HTML輸入框。(見 圖3 ) 。下面的代碼是從new_document.jsp截取出來的,它的作用是頁面請求中是否包含表單數(shù)據(jù)。如果有的話,就向數(shù)據(jù)存儲區(qū)里插入一個Document實例。
- < %
 - String url = request.getParameter("url");
 - String title = request.getParameter("title");
 - String text = request.getParameter("text");
 - if (url!=null && title!=null && text!=null) {
 - PersistenceManager pm =
 - PMF.get().getPersistenceManager();
 - try {
 - Document doc = new Document(url, title, text);
 - pm.makePersistent(doc);
 - IndexToken.indexString(doc.getUri(), doc.getTitle() +
 - " " + doc.getContent());
 - } finally {
 - pm.close();
 - }
 - }
 - %>
 
makePersistent這個方法會被直接調(diào)用并把文件保存到數(shù)據(jù)存儲區(qū)。靜態(tài)方法IndexToken.indexString則把根據(jù)文件標題和內(nèi)容生成的片段插入到數(shù)據(jù)存儲區(qū)里。
圖4 從數(shù)據(jù)存儲區(qū)里:刪除所有文件和索引片段 示例應(yīng)用程序需要一個簡單的方法來清空數(shù)據(jù)存儲區(qū)里所有測試“文件”數(shù)據(jù)
由于此示例程序是公開托管在Google那里,它需要一個簡單的方法來清除文件存儲區(qū)里所有的測試“文件”。delete_all.jsp這個jsp文件能從數(shù)據(jù)存儲里刪除所有的文件和索引片段(參見 圖4 ) 。
- PersistenceManager pm = PMF.get().getPersistenceManager();
 - Query query = pm.newQuery(Document.class);
 - try {
 - List
 results = (List ) - query.execute();
 - if (results.iterator().hasNext()) {
 - for (Document d : results) {
 - pm.deletePersistent(d);
 - }
 - }
 - } finally {
 - query.closeAll();
 - }
 - query = pm.newQuery(IndexToken.class);
 - try {
 - List
 results = (List ) query.execute(); - if (results.iterator().hasNext()) {
 - for (IndexToken indexToken : results) {
 - pm.deletePersistent(indexToken);
 - }
 - }
 - } finally {
 - query.closeAll();
 - }
 
search.jsp的JSP的文件包含了一個HTML搜索框(參見 圖5 ) 。以下是處理搜索操作的代碼:
- String query = "";
 - String results = "< b>Results:< /b>< br/>";
 - Object obj = request.getParameter("search");
 - if (obj != null) {
 - query = "" + obj;
 - List
 hits = IndexToken.search(query); - for (SearchResult hit : hits) {
 - results += "< p>" + hit + "< /p>";
 - }
 - }
 
圖5 搜索結(jié)果: filesearch.jsp包含有一個HTML搜索框。
SearchResults類里新增的ToString 方法用于格式化搜索結(jié)果:
- public String toString() { return url +
 - " - " + score + ": " + title; }
 
成本低廉的解決方案
Google App Engine為我們提供了一套無成本(或低成本)的解決方案。盡管對于某些Web應(yīng)用服務(wù)來說,它可能并不是最佳的部署平臺,但它絕對值得一試,而且絕對有資格成為我們開發(fā)工具箱里的備選項。
【App Engine相關(guān)文章推薦】




















 
 
 
 
 
 
 