In the initial release of the Index Server, the HTM/IDQ/HTX approach to content searching was the only one supported. In the current release, the limitations of static querying have been addressed by the introduction of two new scriptable objects, the Query
and the Utility
objects. Using these objects, we can define and execute queries within ASP code. Instead of piping the results of a query back to the client browser, the Query
object stores the query's results in an ActiveX Data Object (ADO) Recordset
. We can do anything we like with the contents of this Recordset
—we can send all the contents back to the client browser, send a subset of the contents, or perform our own processing on the contents and send back the results of this processing.
The Utility
object supports methods that enable us to perform various tasks in conjunction with a Query
object. The most important of these tasks involves defining the query's scope, which we accomplish using the Utility
object's AddScopeToQuery
method.
The ASP Query
object encapsulates an Index Server search. It supports the properties that you use to describe the information you're looking for, the columns to return, and the order in which to sort the query's results.
We create a Query
object in an Active Server Page as we create most other ASP objects: by using the Server
object's CreateObject
method to store a reference to the object in a variable. The ProgID for this object is ixsso.Query
.
Set objQuery = Server.CreateObject("ixsso.Query")
Having created our Query
object, let's look at some of the new object's capabilities.
The methods and properties supported by the Query
object fall roughly into two categories: those that we use to describe our query, as well as the results we want to get, and those that help us initialize the query from a URL instead of building it up piece by piece. Here's a list of the Query
object's methods:
Method | Description |
CreateRecordset | Executes the query stored in the Query property, and returns the query's results as an ADO Recordset. |
DefineColumn | Associates a new friendly name with a column. |
QueryToURL | Builds a URL out of the Query object's current state. |
Reset | Clears the settings of the Query object, rendering it stateless. |
SetQueryFromURL
Initializes the Query
property from the contents of a URL.
Here's a list of the Query
object's properties:
Property | Description |
AllowEnumeration | Allows the query to use enumeration (as opposed to an index). |
Catalog | A string that specifies the name of the catalog to search. |
Columns | A string that contains a comma-delimited list of the columns to return in the Recordset . |
LocaleID | Specifies the query's locale. |
MaxRecords | Specifies the maximum number of records to return. |
OptimizeFor | Allows you to set performance priorities on the query. |
Query | A string that contains the query to execute. This string consists of some combination of Index Server query language properties and operators. Functionally analogous to an SQL WHERE clause. |
SortBy | A comma-delimited string that specifies the columns on which the search results should be sorted. |
Here's a shortcut for quickly mastering the use of the Query
object, particularly if you've tinkered around with the static querying that we described earlier: every single property supported by the Query
object maps conceptually to some setting in an IDQ file. For example, the Query
object's Query
property serves precisely the same purpose that the CiRestriction
setting does. The Columns
property serves a function identical to that served by CiColumns
. You get the idea.
Of all the properties that we listed in the previous section, the most essential are the Query
and Columns
properties, which specify the query to execute and the columns to return, respectively. These properties are indispensable; no query makes sense without them, so you have to initialize them before calling the object's CreateRecordset
method. (Generally, however, you'll also want to initialize the SortBy
property, to determine the order in which your search results should be listed.)
You can initialize the properties of the Query
object in one of two ways. In the first scenario, you parse the contents of a posted request, manually build a query string, and then explicitly initialize the Query
object's Query
property to reference this query string.
For example, if you wanted to retrieve the filename and document author for all the server-based documents that contain the word "Tolstoy", and that are less than 100 bytes in size, here's the initialization code you would use.
objQuery.Query = "CONTAINS Tolstoy AND @size < 100"
ObjQuery.Columns = "DocAuthor,filename"
In the second approach, you create a <FORM>
that uses a group of conventional names for its form fields, and you use the Query
object's SetQueryFromURL
method to initialize the Query
object for you. If you use this approach, keep the following in mind:
The form fields in your form must be named in accordance with a particular set of conventions. For example, the field that contains the text of your query should be named qu
. The form field that identifies the catalog you want to search should be named ct
. When you post this form to an ASP page, and then call the Query
object's SetQueryFromURL
method, the method automatically maps the contents of the qu
form field to the Query
object's Query
property. Similarly, the contents of the ct
form field are automatically mapped to the object's Catalog
property. Other implicit property mappings are mh
(MaxRecords
), sd
(SortBy
), and ae
(AllowEnumeration
). The full list is enumerated and explained in the Index Server online documentation.
In the meantime, let's take a look at this approach to query-building in action. First, we're going to create an HTML form that adheres to the naming conventions expected by the SetQueryFromURL
method. Then, we're going to post the form to an ASP that creates a Query
object, and calls SetQueryFromURL
on that object. Finally, we're going to enumerate the values stored in the object's properties to demonstrate that SetQueryFromURL
did its job—that is, that it initialized our Query
object for us.
Here's the HTML form, query_url.htm
, that we'll use:
The code that creates it looks like this:
<HTML>
<HEAD>
<TITLE>Setting Queries from URLs</TITLE>
</HEAD>
<BODY>
<H2>Setting Queries from URLs</H2>
<P>The following form illustrates the CGI naming
conventions for setting queries from URLs.</P>
<FORM method="GET" action="query_url.asp">
<TABLE border="0" width="100%">
<TR>
<TD width="28%">Search Text</TD>
<TD width="72%">
<P><INPUT type="text" name="qu" size="34"></P>
</TD>
</TR>
<TR>
<TD width="28%">Maximum Hits</TD>
<TD width="72%"><P>
<INPUT type="text" name="mh" size="20"></P>
</TD>
</TR>
<TR>
<TD width="28%">Sort By:</TD>
<TD width="72%"><p>
<SELECT name="sd" size="1">
<OPTION value="DocAuthor">Document Author</OPTION>
<OPTION value="Filename">Filename</OPTION>
<OPTION value="size">Size</OPTION>
</SELECT></P>
</TD>
</TR>
<TR>
<TD width="28%">Allow Enumeration:</TD>
<TD width="72%"><P>
<SELECT name="ae" size="1">
<OPTION value="1">True</OPTION>
<OPTION value="0">False</OPTION>
</SELECT></P>
</TD>
</TR>
<TR>
<TD width="28%">Catalog:</TD>
<TD width="72%"><P>
<INPUT type="text" name="ct" value="Web" size="20"></P>
</TD>
</TR>
<TR>
<TD width="28%"></TD>
<TD width="72%"><P>
<INPUT type="submit" value="Show Object" name="Action"></P>
</TD>
</TR>
</TABLE>
</FORM>
</BODY>
</HTML>
In this form, we've included the form fields for the Query
property (qu
), the MaxRecords
property (mh
), The SortBy
property (sd
), the AllowEnumeration
property (ae
), and the Catalog
property (ct
). Note: the names that you assign your form fields should match exactly the ones I've used here.
Now, here's the query_url.asp
file, the ASP to which this HTML file posts its contents.
<%
'Create a Query object, initialize it using
'SetQueryFromURL, and dump the object state
Set objQuery = Server.CreateObject("ixsso.Query")
objQuery.SetQueryFromURL(Request.QueryString)
%>
<HTML>
<HEAD><TITLE>QueryFromURL Results</TITLE></HEAD>
<BODY>
<P>The following reflects the property settings
on the Query object.</P>
<TABLE border="0" width="100%">
<TR>
<TD width="30%">Query.Query</TD>
<TD width="70%"><%= objQuery.Query%></TD>
</TR>
<TR>
<TD width="30%">Query.MaxRecords</TD>
<TD width="70%"><%= objQuery.MaxRecords%></TD>
</TR>
<TR>
<TD width="30%">Query.SortBy</TD>
<TD width="70%"><%= objQuery.SortBy %></TD>
</TR>
<TR>
<TD width="30%">Query.AllowEnumeration</TD>
<TD width="70%"><%= objQuery.AllowEnumeration%></TD>
</TR>
<TR>
<TD width="30%">Query.Catalog</TD>
<TD width="70%"><%= objQuery.Catalog%></TD>
</TR>
</TABLE>
</BODY>
</HTML>
And here's the result it produces:
A last word before we go on to look at what the other object, the Utility
object, brings to the picture. Here, our objective was simply to do a Query
object property dump. To make this example a working search, we would have needed to make only minor modifications to query_url.asp
. Such modifications would include initializing the Query
object's Columns
property, eliminating the HTML text that dumps the Query
object's settings, and finally, using the CreateRecordset
method to return a Recordset
containing the query's results. Shortly, we'll take a look at an example that illustrates this whole process.
You can use the Query
and Utility
objects independently (as in our previous example), but you probably won't. These objects are designed to coordinate. The most important feature of the Utility
object is that it supports a method called AddScopeToQuery
, which enables you to add scope to a query defined in the Query
object. In addition, this object makes it possible to do other interesting things, such as retrieving Recordset
elements by index, and truncating strings to a specified white space.
To create a Utility
object, use the Server object's CreateObject
method, specifying "isxxo.Util"
as the ProgID.
Set objUtility = Server.CreateObject("isxxo.Util")
The Utility
object contains no properties. The following table lists and briefly describes the methods that the object supports:
Method | Description |
AddScopeToQuery | Associates a Utility object with a Query object, defining the physical scope of the query. |
GetArrayElement | Returns an element in an array. Usually, you'll use this method to access specific elements of a Recordset by index. |
ISOToLocaleID | Returns the Win32 locale ID for a specified ISO 639 language code. |
LocaleIDToISO | Returns the ISO 639 language code for the specified Win32 locale ID. |
TruncateWhiteSpace | Truncates a string at the specified white space. This method is particularly useful when you want to limit how much of a large piece of text will be displayed. |
We've addressed query scope previously in this chapter. Limiting a query's scope can enhance significantly the speed with which a search executes. The Utility
object's AddScopeToQuery
method
accomplishes this, identifying which directories are to be searched, and specifying the depth of the search within the specified directory tree. This method is functionally equivalent to the CiScope
setting in a static query's IDQ file.
The following code fragment demonstrates the use of the Utility
object's AddScopeToQuery
method. This call to AddScopeToQuery
identifies the Query
object on which to set the scope, the directory to be searched, and the depth to which the search should extend. The use of the "shallow"
parameter indicates that only the named directory (rather than its sub-directories) will be searched. The specification of scope in this fashion replaces the default scope, which encompasses a deep search of the default catalog:
Set objUtil = Server.CreateObject("ixsso.util")
objUtil.AddScopeToQuery objQuery, _
"d:\Inetpub\wwwroot\IndexSample", "shallow"
Imagine a situation in which you want to retrieve documents that contain a specified text string, and that have been modified within a specified number of days or hours. Here's how you might represent such information graphically in an HTML form:
This page can be run or downloaded from our web site at http://rapid.wrox.co.uk/books/1266/
and is named query_sample.htm
.
In one text box, the user can type any identifying content. Using the radio buttons, they can specify that they want to locate only those documents written in a given period of time. Finally, the user fills in a text box with a path that identifies the scope of the search. Note that this directory can be either a fully qualified or relative path.
Here's the text of the HTML file, query_sample.htm
, which creates the form displayed above:
<HTML>
<HEAD><TITLE>The Query and Utility Objects</TITLE></HEAD>
<BODY>
<H2>Using the Query and Utility Objects</H2>
<P>In the following box, please identify any
words you wish to search for, and that appear
in the content of the article you want to locate.</P>
<FORM method="post" action="query_sample.asp">
Locate articles that contain the following text:
<INPUT type="text" name="SearchString" size="20"><P>
Limit results to the following:<BR>
<INPUT type="radio" value="Days" checked name="Limit">
Those published in the last
<INPUT type="text" name="Days" size="10"> days.<BR>
<INPUT type="radio" name="Limit" value="Hours">
Those published in the last
<INPUT type="text" name="Hours" size="10"> hours.<P>
Directory to be searched:
<INPUT type="text" name="Scope" size="25"><P>
<INPUT type="submit" value="Search" name="Action">
</FORM>
</BODY>
</HTML>
In the ASP file that processes this form, we first read the values stored in our form fields so that we can build up a query from these values. If the SearchString
form field is empty, we assume the user is interested only in documents modified within a specified number of hours or days. If SearchString
is not empty, we assume that the user is looking for the search string in the contents of the document itself:
<%
'Create the query based on user input
If Request("SearchString") <> "" Then
Query = "@contents " & Request("SearchString") & " and "
End If
If Request("Limit") = "Days" Then
Query = Query & " @write > " & "-" & Request("Days") & "d"
Else
Query = Query & " @write > " & "-" & Request("Hours") & "h"
End If
...
Having built up our query, we create a Query
object and initialize its properties, specifying that the maximum number of records returned should be 5, that the results should be sorted in descending order by filename, and indicating the columns of information we want to return:
...
Set objQuery = Server.CreateObject("ixsso.Query")
objQuery.Query = Query
objQuery.MaxRecords = 5
objQuery.SortBy = "filename[d]"
objQuery.Columns = "vpath,path,filename,size,write,characterization"
...
Next, we determine the scope for the query. Remember, if the scope is not explicitly defined (using the AddScopeToQuery
method), then the Query
object will perform a deep (or recursive) search of the entire default catalog. For illustrative purposes that's not necessary, so here we specify that we want to perform a shallow search of whatever directory the user specified:
...
If Request("Scope") <> "" Then
Set objUtil = Server.CreateObject("ixsso.util")
objUtil.AddScopeToQuery objQuery, Request("Scope"), "shallow"
End If
...
Now we call the Query
object's CreateRecordset
method to create an ADO recordset, and iterate through it putting the values into the page. Note that each column that we specified in the Columns
property maps to a field in the returned recordset. If there are no records, we output an error message:
...
Set objRS = objQuery.CreateRecordset("nonsequential")
If objRS.EOF Then
Response.Write("No records found!")
Response.End
End If
%>
...
Finally, we can add the HTML that creates the return page that the user sees:
...
<HTML><HEAD><TITLE>Search Results</TITLE></HEAD>
<BODY>
<H1>Search Results</H1>
<TABLE border="0" width="100%">
<% do while not objRS.EOF %>
<TR>
<TD width="20%" align="right"><b>Virtual Path:</B></TD>
<TD width="80%"><%= objRS("vPath")%></TD>
</TR>
<TR>
<TD width="20%" align="right"><b>Physical Path:</B></TD>
<TD width="80%"><%= objRS("Path")%></TD>
</TR>
<TR>
<TD width="20%" align="right"><b>Filename:</B></TD>
<TD width="80%"><%= objRS("Filename")%></TD>
</TR>
<TR>
<TD width="20%" align="right"><B>Size:</B></TD>
<TD width="80%"><%= objRS("Size")%></TD>
</TR>
<TR>
<TD width="20%" align="right"><B>Last Modified:</B></TD>
<TD width="80%"><%= objRS("Write")%></TD>
</TR>
<TR>
<TD width="20%" align="right"><B>Exceprt:</B></TD>
<TD width="80%"><%= objRS("Characterization")%></TD>
</TR>
<% objRS.MoveNext %>
<% Loop %>
</TABLE>
</BODY>
</HTML>
And here's the result of our code—a page containing some of the properties of the matching documents:
For the sake of simplicity, this sample does not demonstrate how to separate search results across multiple pages (usually called paging). We've seen how it's done in static searches earlier, and we'll have a brief look at how we can do it in an object-based search at the end of this chapter. There are also examples included with the Index Server documentation. For instructions on how to access these samples, see the Summary at the end of this chapter.