Object-Based Searches

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 Query Object

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.

Query Object Methods and Properties

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.

SetQueryFromURLInitializes 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.

Initializing the Query Object

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.)

Setting Query Properties Explicitly

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"

Setting Query Properties Implicitly

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.

An Implicit Query Object Properties Example

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.

The Utility Object

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")

Utility Object Methods Summary

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.

How to Add Scope to a Query

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"

A Query and Utility Object Sample

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.

The HTML Form Code

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>

The Active Server Pages File

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.

© 1998 by Wrox Press. All rights reserved.