Details about the syntax of these compound ErrorMessage strings are given in the VB ELF Compatibility section. This example illustrates several points about the technique. Note that the column headers are defined (following the semi-colons) in the first "row" -- the first group of { } values enclosed by parens. However, the rows themselves are not guaranteed to appear in typed order; instead they'll appear in order based on the first column. To control the order, you can add a numbering column:
({1;#};{Human resources;To contact this department:};{555-1234;please call this number}); ({2};{Corporate headquarters};{555-1000});
Simple String Substitutions
Skill Level: Basic
Use string substitutions to match specific multi-word phrases, or to ensure that these substitutions are performed before any lexical matching. As an example, similar to the "not discontinued" macro above:
When I Type: [still in stock]
I Really Mean: not discontinued
This substitution is necessary because "in stock" is understood as a synonym for "UnitsInStock". If it were entered as a lexical phrase (without the brackets), it would never fire -- since "in stock" becomes "UnitsInStock" before the system "looks over its shoulder" to see whether the current and preceding word (now, still UnitsInStock) form a compound trigger. By bracketing the trigger, we ensure that it fires before any lexical replacements, so "Which products are still in stock?" yields "Which products are not discontinued?" instead of showing the products with non-zero UnitsInStock values.
Simple string replacements can repair errors of understading, like mistakenly using the BirthDate to answer "Show date we hired Davolio."
When I Type: [date we hired]
I Really Mean: hiredate of
This technique can also avoid mismatching overused field names against the wrong source. This might happen in the following case for instance, because customers have no "first" or "last" name entry, only a single [Contact Name] field.
When I Type: [customers first and last names]
I Really Mean: customers contactname
Note that the relative inflexibility of string matching means that this rule would not fire on correctly punctuated input like "Show the customers' first and last names." Here's a case where we might graduate to a more complex formulation, such as:
When I Type: [first and last names]
I Really Mean: [contactnames | surname ] * customer
When I Type: [surname]
I Really Mean: [ lastname | ContactName ] * employee
This changes "show customers first and last names" (or equally well "show customer's first and last names") in one step into "show customers ContactNames". It also changes "show suppliers first and last names", in two steps, into first "show suppliers surname" and finally "show suppliers ContactName". The bracketed alternative phrases combined with the * syntax indicates that the first choice is used if the string following * is matched anywhere in the question; if not, the second choice is applied.
Phrase Scripts
Skill Level: Advanced
Phrase scripts allow you to trigger a customized VBScript or JScript function whenever a specific string appears in a question. You can define as many of these scripts as you like, each one attached to as many string triggers as necessary.
Let's start with a simple example, before we see how to generalize the technique. In the Northwind database, let's ask the reasonable question: "What is the price of shrimp?" The answer that comes back is pretty poor:
SELECT DISTINCT Products.UnitPrice , Products.ProductName FROM Products ;
This SQL shows the price of every product in our inventory, none of which happen to be shrimp. The problem is that to the extent that Access ELF "understands" anything, it understands only those things mentioned in the database. If an item (like "shrimp") doesn't appear, the odds are slim that ELF will connect it to a related topic (like Seafood).
In general, the stategy of ignoring what it doesn't understand works pretty well for Access ELF. But there are cases where we want to recognize something, and react in a certain way, even to words that have no meaning in our database context. The Phrase script gives us this capability, because it can react to any arbitrary string.
The mechanics of Phrase scripts are as follows. Phrase scripts are written and named just like the other types of script, by adding one into the script library accessible from the Scripts tab of Settings. The name of this script is typed into the Script Name column, while a function with the same name must appear somewhere within the script itself. Once this script is registered in the script library, it can be made active by entering its name in the Script Name column of the Phrases window. Because Phrase scripts aren't checked-by-name at the time they're registered, you can also accomplish this in the opposite order. First enter a name on the Phrase window. then paste an identically-named script into the script panel.
We can add a script to handle the specific pattern "shrimp" in a question. Notice that we can't rely on a Spell script to intercept references to this unknown item, because "shrimp" appears in our large dictionary of general-use words. All we know about "shrimp", though, is that it's not a misspelling of something else, and that it's a noun.
With this Phrase definition, we can catch any question that contains the pattern "shrimp".
When I Type: [shrimp]
I Really Mean:
Script Name: GotShrimp
We'll define the GotShrimp Phrase script as follows:
function GotShrimp
Resource="shrimp"
end function
Finally, we'll add an Answer script which checks the Resource variable, and reacts when "shrimp" is detected.
function LastMinuteChanges
if Resource="shrimp" then
QueryResult=-8
ErrorMessage="Sorry, we don't currently stock any shrimp products"
end if
end function
This is just one way we could have coded around the problem. An alternate way of recording the "shrinp-request" condition would be to set the PrivateErrorCode variable to some constant (we might include some code with definitions like UNKNOWN_ITEM_SHRIMP = 1001). Then we'd check the value of PrivateErrorCode in our AnswerScript, and branch on our various unstocked items.
Another plausible way to respond would be to simply display a dataset showing the seafood items, using the WarningString variable to pass back an explanation:
function LastMinuteChanges
if Resource="shrimp" then
QueryResult=1
SQL=
"SELECT DISTINCTROW Products.* FROM Categories , Products , Categories INNER JOIN Products ON Categories.CategoryID = Products.CategoryID WHERE ( Categories.CategoryName = 'Seafood' ) ; "
WarningCode=1; ' any non-zero value
WarningString="Sorry, we don't currently stock any shrimp products"
end if
end function
Now, "shrimp" is not likely to be a completely unique case. Users unfamiliar with our business or product line might often ask questions about places or things that aren't in the database. A specialty food retailer (like Northwind) might get some questions about Icelandic customers (they do have customers in Sweden, Finland and Norway). It may be impossible to predict ahead of time which "unknowns" will appear in questions, but by keeping careful logs one should be able, over time, to collect good intelligence on what users ask about, and which of them (in the context of the database, at least) simply doesn't exist. If that's the case, we can substantially improve our database interface by creating a "table of unknowns" that can be used by our Phrase and Answer scripts to decide how to handle unrecognized input.
Consider this variation of the above code:
function GotUnknown
Resource=PhraseTrigger
end function
Function Definitions
Skill Level: Intermediate
Functions are new in version 4.0, and solve some of the earlier problems related to applying Access Basic code to SQL column data. A function definition typically looks something like this:
When I Type: [age]
I Really Mean: Int(DateDiff('d',[Employees].[BirthDate],Now())/365.25)
Script Name: Function
Since the first releases of Access ELF, we've emphasized that although it's possible to use Phrase substitutions to apply Access Basic functions, it was generally better practice to embed calls to these functions into queries, and include those queries in the analysis. This tied the details of function-calling (parameters, types etc) into easily-tested packages (ie., the queries), and allowed our natural language interfaces to treat such calculated results just like any other field.
There were, and continue to be, many good reasons to embed function definitions into SQL queries. The classic example is the ever-popular "age question". Since the BirthDates of Northwind employees are available, nearly every test program includes a question such as "How old are the employees?"
Previously, we recommended that the user first create a query (named, for instance, Ages) which defined the meaning of Age for this application. Age can mean different things when applied to people say, or to video tapes. In this case, someone might try a query like:
Select Year(Now) - Year(BirthDate) as Age, EmployeeID from Employees;
This would be a good first try. It's not especially important how accurate it is, because it's easy to test and modify. Pretty soon we'd see that the difference between the years is not exactly the same thing as what we usually mean by "age" (it changes for everyone on New Year's Day). So we would work our way toward a more perfect formulation, such as:
Select Int(DateDiff('d',[Employees].[BirthDate],Now())/365.25) as Age, EmployeeID from Employees
The point is that this definition of "age" is independent of the natural language interface. Once the query is defined, we simply include it in the View, and it acts just like any other dataset. The calculations aren't even visible in the SQL translations, which look something like this:
SELECT DISTINCTROW Ages.Age , Employees.EmployeeID , Employees.LastName FROM Employees , Ages , Employees INNER JOIN Ages ON Employees.EmployeeID = Ages.EmployeeID ;
Armed with the Ages query and a few trivial Phrase macros (such as older than => with age greater than), we could handle highly complex questions, like "which employees older than 35 have customers in Norway?"
SELECT DISTINCTROW Employees.EmployeeID , Customers.CustomerID , Ages.Age , Orders.ShipName , Customers.CompanyName , Employees.LastName FROM Ages , Employees , Orders , Customers , Ages INNER JOIN Employees ON Ages.EmployeeID = Employees.EmployeeID , Employees INNER JOIN Orders ON Employees.EmployeeID = Orders.EmployeeID , Orders INNER JOIN Customers ON Orders.CustomerID = Customers.CustomerID WHERE ( ( Ages.Age > 35 and Customers.Country = "Norway" ) ) ;
While it always seemed to us that this was a huge payoff for the small investment involved in defining a function query and a few Phrase macros, it's always been a sore point with our users (and with the authors of academic papers on the state of natural language processing; see Hakan Lane's Report on NLIDBs and Seymour Knowles' A Natural Language Database Interface For SQL-Tutor). The often-raised argument was this: if a user were adept enough to write an "age" query, why would they need a natural language interface in the first place?
Of course, this ignores the difference between a) the user's responsibility to define the meaning of "age", and b) the interface's job of applying it as a condition, while creating a complex, yet syntactically correct query like the "Norway" example above. And ignores as well the problem that we can't accurately know what "age" means to our user (who might age wines or cheeses, or measure the ages of rocks or documents).
All this explanation is by way of introducing our new solution to this perennial problem -- implicit queries. Beginning with Access ELF 2002, all the user must do to define a function (such as age) is to type its definition -- any evaluatable expression -- into the Phrase column of the Phrases window. The Phrase macro must use the defined term itself (eg "age") as the string trigger, with the ScriptName column set to the special value "Function". Upon the next analysis, this definition will be used to create an implicit query, that is, a query which behaves exactly like the SQL statement we formerly expected the user to author (except that it doesn't actually appear in the database container).
What's more, now that the function definitions are entered in the same window as other Phrase macros, we feel it's safer to auto-generate "speculative" definitions for commonly calculated functions such as age. The reason is that the user can clearly see our definition, and change it easily (eg, to apply to wines or dinosaurs). This means that all the processing steps for handling a typical age question (such as "How old is Nancy Davolio?") are now handled by the automatic interface generator.
The reason we refer to this technique as "implicit queries" is that the definition query (Ages) is now created on-the-fly from the Phrase expression, and embedded into the translation query, as in the following (again, in answer to "which employees older than 35 have customers in Norway?")
SELECT DISTINCTROW Employees.EmployeeID , Customers.CustomerID , [age1].age , Orders.ShipName , Customers.CompanyName , Employees.LastName FROM (SELECT Int(DateDiff('d',[Employees].[BirthDate],Now())/365.25) AS [age], [EmployeeID] FROM Employees) AS [age1] , Employees , Orders , Customers , [age1] INNER JOIN Employees ON [age1].EmployeeID = Employees.EmployeeID , Employees INNER JOIN Orders ON Employees.EmployeeID = Orders.EmployeeID , Orders INNER JOIN Customers ON Orders.CustomerID = Customers.CustomerID WHERE ( ( [age1].age > 35 and Customers.Country = "Norway" ) ) ;
Note: you may still have to define a few ease-of-use macros such as:
When I Type: [older than]
I Really Mean: with age greater than
We can call into our own Access Basic functions by wrapping them in these phrase definitions. As an example, let's say we have a Security_Clearance function which accepts an employee's surname and hire date, and returns a security clearance (integer).
When I Type: [clearance]
I Really Mean: Security_Clearance(Employees.Lastname,Employees.HireDate)
Script Name: Function
This allows us to ask questions like "Show the employees with clearance between 7 and 9." The beauty of this approach is that we don't have to define special constructions for the use of "clearance" in a select list, as part of a condition, as part of an aggregate ("Show average clearance by country.") and so on. All the rules that allow us to handle database fields with perfect generality now apply to this artificial field.
This Access Basic function example will work perfectly using the Datasheet response and graph styles of Access ELF. However, there is one more step you need to take to make this work seamlessly with the Worksheet.
Access Basic functions cannot display in the Worksheet unless they are exported to a separate function library.
This property is not peculiar to Access Basic embedded into implicit queries -- it applies to any Access Basic function used in the Access ELF program. (For instance, you can type functions directly into the SQL window, and execute them as datasheets, but not as Worksheets.) This is discussed in more detail in the SQL Text topic and the FAQ. This applies only to the Access Basic functions you write yourself and embed in SQL. It does not apply to the other types os implicit queries, those limited to SQL and/or Access Basic keywords and built-in language functions.
The string trigger for a function definition must be a single word. To use a compound word as the trigger of a function, use a two-part phrase definition:
When I Type: [sales amount]
I Really Mean: SalesAmount
When I Type: [SalesAmount]
I Really Mean: CLng([Order Details].[UnitPrice]*[Order Details].Quantity*(1-[Order Details].Discount)*100)/100
Script Name: Function
Also note that unlike other phrase definitions, implicit queries do not take effect until after the next analysis. Unlike the technique of defining a phrase using only field names and mathematical operators (see DiscountedPrice, below), implicit queries can reference fields from only a single table. We hope to eliminate these limitations in a future release.
Finally, let's consider a few of the mistakes you could make while upgrading a function written in the simpler (pre-v4.0) style using this new, more capable technique. Going back to the example introduced on the Phrases topic:
When I Type: [discounted price]
I Really Mean: price - (price * discount) as "Discounted Price"
Let's say we needed to change this function so that the discount was multiplied by a seasonal factor. Assume that Seasonal_Factor is a function defined in our Access Basic library as Seasonal_Factor%(). No parameters are required because the function simply checks the system clock for the date, and returns a seasonally-based weighting factor. We'll add the new function into the expression, and add the Function keyword to the ScriptName column. (Don't try this example without reading the rest of this section.)
This example is WRONG.
When I Type: [discounted price]
I Really Mean: UnitPrice - (UnitPrice * Seasonal_Factor() * discount) as "Discounted Price"
Script Name: Function
The above example is wrong because we've tried to illustrate several possible errors. There are three mistakes. The first is that we haven't deleted the column alias: as "Discounted Price". With the implicit query method, the alias is defined by the trigger, and can't be overridden. The second mistake is that the field names are not qualified by table specifiers. The Access ELF preprocessor will try to pick the right table, but in this case there are two possibilities, Products and [Order Details]. We could get away with this before, when Access ELF itself was interpreting the phrase as part of a natural language question, and could make judgements based on the rest of the question. Now, we're deciding on the meaning of this expression during an analysis, so we have no way to "break ties." For this reason, always preface field references with a specific table. And the third mistake? Remember -- string triggers for functions must be single words!
When I Type: [DiscountedPrice]
I Really Mean: [Order Details].UnitPrice - ([Order Details].UnitPrice * Seasonal_Factor() * [Order Details].Discount)
Script Name: Function
One technical glitch with implicit queries has been observed under Microsoft Access XP. Definition queries embedded into SQL run properly, but they cannot be resaved; Access will claim that the SQL is malformed. The problem is that Access ELF routinely resaves datasheet queries after adjusting their column headings (if the "BestFit Columns" box is checked). It does this to save the user the step of responding to an inquiry from Access upon closing the datasheet: "Do you want to save changes to the layout of query X?". Since we can't auto-save these queries, this message box will appear when closing the datasheet. There is no reason to ever respond Yes to this inquiry, since the datasheet is a temporary object which is probably about to be overwritten anyway -- by the SQL translation of your next question. However, if you do respond Yes, Microsoft Access XP may crash while trying to save the datasheet definition. This problem has been reported to Microsoft.
Explicit queries
Finally, don't despair if you encounter a situation which runs into limitations with either Function method. If you want to define an expression using both Access Basic functions and fields from a number of tables, there's always the explicit query method -- that is, writing a query and including it in the Analysis. To change UnitPrice from [Order Details].UnitPrice to [Products].UnitPrice in the above definition, use the Microsoft Access query writer: Include the defined expression in the new query (named eg [Discounted Prices]) along with the Primary Keys of any referenced tables..
SELECT ([Products].UnitPrice-([Order Details].UnitPrice*SeasonalFactor()*[Order Details].discount)) AS [Discounted Price], [Order Details].OrderID, Products.ProductID
FROM Products INNER JOIN [Order Details] ON Products.ProductID = [Order Details].ProductID;
For queries with multiple primary key references like this, use the Microsoft Access Relationship Map (Tools/Relationships) to add the proper relationships. In this case, we'd add [Discounted Prices] to the map, linking it to [Order Details] on the OrderID field and to Products on the ProductID field. (Do the same for any other multi-key query you include, such as [Order Details Extended].)
After the next analysis,
"Show name, date, order id and discounted price of products." translates as:
SELECT DISTINCTROW Orders.OrderDate , Orders.OrderID , [Discounted Prices].[Discounted Price] , Products.ProductName , Orders.ShipName FROM Products , [Discounted Prices] , [Order Details] , Orders , Products INNER JOIN [Discounted Prices] ON Products.ProductID = [Discounted Prices].ProductID , Products INNER JOIN [Order Details] ON Products.ProductID = [Order Details].ProductID , [Order Details] INNER JOIN [Discounted Prices] ON [Order Details].OrderID = [Discounted Prices].OrderID , Products INNER JOIN [Order Details] ON Products.ProductID = [Order Details].ProductID , [Discounted Prices] INNER JOIN [Order Details] ON [Discounted Prices].OrderID = [Order Details].OrderID , [Order Details] INNER JOIN Orders ON [Order Details].OrderID = Orders.OrderID ;
Last Updated: March, 2002