1 // Copyright 2011 Google Inc. All Rights Reserved.
3 package com
.google
.appengine
.demos
.search
;
5 import com
.google
.appengine
.api
.search
.Document
;
6 import com
.google
.appengine
.api
.search
.Field
;
7 import com
.google
.appengine
.api
.search
.Index
;
8 import com
.google
.appengine
.api
.search
.IndexSpec
;
9 import com
.google
.appengine
.api
.search
.OperationResult
;
10 import com
.google
.appengine
.api
.search
.Query
;
11 import com
.google
.appengine
.api
.search
.QueryOptions
;
12 import com
.google
.appengine
.api
.search
.Results
;
13 import com
.google
.appengine
.api
.search
.DeleteException
;
14 import com
.google
.appengine
.api
.search
.ScoredDocument
;
15 import com
.google
.appengine
.api
.search
.SearchServiceFactory
;
16 import com
.google
.appengine
.api
.search
.StatusCode
;
17 import com
.google
.appengine
.api
.users
.User
;
18 import com
.google
.appengine
.api
.users
.UserService
;
19 import com
.google
.appengine
.api
.users
.UserServiceFactory
;
21 import java
.io
.IOException
;
22 import java
.util
.ArrayList
;
23 import java
.util
.Arrays
;
24 import java
.util
.Date
;
25 import java
.util
.Iterator
;
26 import java
.util
.List
;
27 import java
.util
.StringTokenizer
;
28 import java
.util
.logging
.Level
;
29 import java
.util
.logging
.Logger
;
31 import javax
.servlet
.ServletException
;
32 import javax
.servlet
.http
.HttpServlet
;
33 import javax
.servlet
.http
.HttpServletRequest
;
34 import javax
.servlet
.http
.HttpServletResponse
;
37 * A demo servlet showing basic text search capabilities. This servlet
38 * has a single index shared between all users. It illustrates how to
39 * add, search for and remove documents from the shared index.
42 public class TextSearchServlet
extends HttpServlet
{
44 private static final long serialVersionUID
= 1L;
46 private static final String VOID_REMOVE
=
47 "Remove failed due to a null doc ID";
49 private static final String VOID_ADD
=
50 "Document not added due to empty content";
53 * The index used by this application. Since we only have one index
54 * we create one instance only. We build an index with the default
55 * consistency, which is Consistency.PER_DOCUMENT. These types of
56 * indexes are most suitable for streams and feeds, and can cope with
57 * a high rate of updates.
59 private static final Index INDEX
= SearchServiceFactory
.getSearchService()
60 .getIndex(IndexSpec
.newBuilder().setName("shared_index"));
66 private static final Logger LOG
= Logger
.getLogger(
67 TextSearchServlet
.class.getName());
70 public void doGet(HttpServletRequest req
, HttpServletResponse resp
)
71 throws IOException
, ServletException
{
72 User currentUser
= setupUser(req
);
73 String outcome
= null;
74 switch (getAction(req
)) {
76 outcome
= add(req
, currentUser
);
79 outcome
= remove(req
);
81 // On DEFAULT we fall through and just execute search below.
83 String searchOutcome
= search(req
);
84 if (outcome
== null) {
85 outcome
= searchOutcome
;
87 req
.setAttribute("outcome", outcome
);
88 req
.getRequestDispatcher("index.jsp").forward(req
, resp
);
91 private User
setupUser(HttpServletRequest req
) {
92 UserService userService
= UserServiceFactory
.getUserService();
93 User currentUser
= userService
.getCurrentUser();
94 if (currentUser
!= null) {
95 req
.setAttribute("authAction", "sign out");
96 req
.setAttribute("authUrl",
97 userService
.createLogoutURL(req
.getRequestURI()));
99 currentUser
= new User("nobody@example.com", "example.com");
100 req
.setAttribute("authAction", "sign in");
101 req
.setAttribute("authUrl",
102 userService
.createLoginURL(req
.getRequestURI()));
104 req
.setAttribute("nickname", currentUser
.getNickname());
109 * Indexes a document built from the current request on behalf of the
110 * specified user. Each document has three fields in it. The content
111 * field stores used entered text. The email, and domain are extracted
112 * from the current user.
114 private String
add(HttpServletRequest req
, User currentUser
) {
115 String content
= req
.getParameter("doc");
116 if (content
== null || content
.isEmpty()) {
117 LOG
.warning(VOID_ADD
);
120 String ratingStr
= req
.getParameter("rating");
122 if (ratingStr
!= null) {
123 rating
= Integer
.parseInt(ratingStr
);
125 Document
.Builder docBuilder
= Document
.newBuilder()
126 .addField(Field
.newBuilder().setName("content").setText(content
))
127 .addField(Field
.newBuilder().setName("email")
128 .setText(currentUser
.getEmail()))
129 .addField(Field
.newBuilder().setName("domain")
130 .setText(currentUser
.getAuthDomain()))
131 .addField(Field
.newBuilder().setName("published").setDate(
132 Field
.date(new Date())))
133 .addField(Field
.newBuilder().setName("rating")
135 String tagStr
= req
.getParameter("tags");
136 if (tagStr
!= null) {
137 StringTokenizer tokenizer
= new StringTokenizer(tagStr
, ",");
138 while (tokenizer
.hasMoreTokens()) {
139 docBuilder
.addField(Field
.newBuilder().setName("tag")
140 .setAtom(tokenizer
.nextToken()));
143 Document doc
= docBuilder
.build();
144 LOG
.info("Adding document:\n" + doc
.toString());
147 return "Document added";
148 } catch (RuntimeException e
) {
149 LOG
.log(Level
.SEVERE
, "Failed to add " + doc
, e
);
150 return "Document not added due to an error " + e
.getMessage();
154 private String
getOnlyField(Document doc
, String fieldName
, String defaultValue
) {
155 if (doc
.getFieldCount(fieldName
) == 1) {
156 return doc
.getOnlyField(fieldName
).getText();
158 LOG
.severe("Field " + fieldName
+ " present " + doc
.getFieldCount(fieldName
));
163 * Searches the index for matching documents. If the query is not specified
164 * in the request, we search for any documents.
166 private String
search(HttpServletRequest req
) {
167 String queryStr
= req
.getParameter("query");
168 if (queryStr
== null) {
171 String limitStr
= req
.getParameter("limit");
173 if (limitStr
!= null) {
175 limit
= Integer
.parseInt(limitStr
);
176 } catch (NumberFormatException e
) {
177 LOG
.severe("Failed to parse " + limitStr
);
180 List
<Document
> found
= new ArrayList
<Document
>();
181 String outcome
= null;
183 // Rather than just using a query we build a search request.
184 // This allows us to specify other attributes, such as the
185 // number of documents to be returned by search.
186 Query query
= Query
.newBuilder()
187 .setOptions(QueryOptions
.newBuilder()
189 // for deployed apps, uncomment the line below to demo snippeting.
190 // This will not work on the dev_appserver.
191 // setFieldsToSnippet("content").
194 LOG
.info("Sending query " + query
);
195 Results
<ScoredDocument
> results
= INDEX
.search(query
);
196 for (ScoredDocument scoredDoc
: results
) {
197 User author
= new User(
198 getOnlyField(scoredDoc
, "email", "user"),
199 getOnlyField(scoredDoc
, "domain", "example.com"));
200 // Rather than presenting the original document to the
201 // user, we build a derived one that holds author's nickname.
202 List
<Field
> expressions
= scoredDoc
.getExpressions();
203 String content
= null;
204 if (expressions
!= null) {
205 for (Field field
: expressions
) {
206 if ("content".equals(field
.getName())) {
207 content
= field
.getHTML();
212 if (content
== null) {
213 content
= getOnlyField(scoredDoc
, "content", "");
215 Document derived
= Document
.newBuilder()
216 .setId(scoredDoc
.getId())
217 .addField(Field
.newBuilder().setName("content").setText(content
))
218 .addField(Field
.newBuilder().setName("nickname").setText(
219 author
.getNickname()))
220 .addField(Field
.newBuilder().setName("published").setDate(
221 scoredDoc
.getOnlyField("published").getDate()))
225 } catch (RuntimeException e
) {
226 LOG
.log(Level
.SEVERE
, "Search with query '" + queryStr
+ "' failed", e
);
227 outcome
= "Search failed due to an error: " + e
.getMessage();
229 req
.setAttribute("found", found
);
234 * Removes documents with IDs specified in the given request. In the demo
235 * application we do not perform any authorization checks, thus no user
236 * information is necessary.
238 private String
remove(HttpServletRequest req
) {
239 String
[] docIds
= req
.getParameterValues("docid");
240 if (docIds
== null) {
241 LOG
.warning(VOID_REMOVE
);
244 List
<String
> docIdList
= Arrays
.asList(docIds
);
246 INDEX
.delete(docIdList
);
247 return "Documents " + docIdList
+ " removed";
248 } catch (DeleteException e
) {
249 List
<String
> failedIds
= findFailedIds(docIdList
, e
.getResults());
250 LOG
.log(Level
.SEVERE
, "Failed to remove documents " + failedIds
, e
);
251 return "Remove failed for " + failedIds
;
256 * A convenience method that correlates document status to the document ID.
258 private List
<String
> findFailedIds(List
<String
> docIdList
,
259 List
<OperationResult
> results
) {
260 List
<String
> failedIds
= new ArrayList
<String
>();
261 Iterator
<OperationResult
> opIter
= results
.iterator();
262 Iterator
<String
> idIter
= docIdList
.iterator();
263 while (opIter
.hasNext() && idIter
.hasNext()) {
264 OperationResult result
= opIter
.next();
265 String docId
= idIter
.next();
266 if (!StatusCode
.OK
.equals(result
.getCode())) {
267 failedIds
.add(docId
);
274 * Extracts the type of action stored in the request. We have only three
275 * types of actions: ADD, REMOVE and DEFAULT. The DEFAULT is included
276 * to indicate action other than ADD or REMOVE. We do not have a special
277 * acton for search, as we always execute search. This way we show documents
278 * that match terms entered in the search box, regardless of the operation.
280 * @param HTTP request received by the servlet
281 * @return the requested user action, as inferred from the request
283 private Action
getAction(HttpServletRequest req
) {
284 if (req
.getParameter("index") != null) {
287 if (req
.getParameter("delete") != null) {
288 return Action
.REMOVE
;
290 return Action
.DEFAULT
;