in Search
 
Home Blogs Forums Marketplace Files
 
 
 

TrainerMatt's Blog

  • Copy Search Results to Another Database

    One of the cool new features of Alchemy 8.2.2, aka SP4, is the ability to copy search results to another database. This is very similar to one of the problems I ask my Alchemy SDK students to solve during a group exercise. The goal of the exercise is to write a program that will do a search, and then copy all of the results to another database.

    There are at least two ways of solving this problem. You could do a Item.Retrieve to save it out to a temp directory, then Item.CreateItem to add it to a new database. That's great, but it means that you have another location for the files you now have to clean up. What would be better is if you could stream the content from one database to another. Well, you can do that and that's what I am going to do right here.

    The key is to use the Builder object. First BeginStreamData, then for each item do a BeginRecord, AppendData, then EndRecord. When everything is copied over, EndStreamData. When calling AppendData, the data is coming from the source item and you call GetContent.

    Here is the full source code for a command line app that does the job:

       1:  using System;
       2:  using System.Collections.Generic;
       3:  using System.Text;
       4:   
       5:  namespace SearchToDB
       6:  {
       7:      class Program
       8:      {
       9:          static void Main(string[] args)
      10:          {
      11:              if (args.Length == 0)
      12:                  return;
      13:              Alchemy.Application auApp = new Alchemy.Application();
      14:              auApp.LoadOptionsFile("");
      15:   
      16:              Alchemy.Database destDB = auApp.Databases[1];
      17:              Alchemy.SearchGroup sg = auApp.SearchGroups.Add("MyNewSearchGroup");
      18:   
      19:              foreach (Alchemy.Database db in auApp.Databases)
      20:              {
      21:                  sg.Databases.Add(db.Path);
      22:                  if (db.Title == "Search Results")
      23:                      destDB = db;
      24:              }
      25:   
      26:              Alchemy.Query auQuery = auApp.NewQuery();
      27:              auQuery.AddFullTextQuery(args[0]);
      28:              auQuery.SearchGroup(sg);
      29:   
      30:              Alchemy.Builder build = auApp.NewBuilder(destDB);
      31:              build.BeginStreamData(null);
      32:              Alchemy.Item searchFolder = destDB.Root.CreateFolder(Alchemy.AuPosEnum.auPosLastChild);
      33:              searchFolder.Title = "Search for " + args[0] + " on " + DateTime.Now.ToString();
      34:              
      35:              foreach (Alchemy.Result result in auQuery.Results)
      36:              {
      37:                  Alchemy.Item dbFolder = searchFolder.CreateFolder(Alchemy.AuPosEnum.auPosLastChild);
      38:                  dbFolder.Title = result.Database.Title + " (" + DateTime.Now.ToShortTimeString() + ")";
      39:                  foreach (Alchemy.Item item in result.Items)
      40:                  {
      41:                      Alchemy.Item destItem = build.BeginRecord(dbFolder, Alchemy.AuPosEnum.auPosLastChild, "");
      42:                      destItem.Title = item.Title;
      43:                      build.AppendData(item.GetContent(0,item.Size,false),item.Size);
      44:                      build.EndRecord(100);
      45:                  }
      46:              }
      47:              build.EndStreamData();
      48:          }
      49:      }
      50:  }

    In lines 17-24, I am creating a new SearchGroup, then going through all the databases in my profile.ini to add them to the search group and then seeing if it is my search results database. After that I do the search, create a builder object, and BeginStreamData. Before going through all of the items, I create a folder in the database that is named for the current search and date.

    Since each result is a list of items within one database, I create a folder named for that database. Then I go to each item in the result, do a build.BeginRecord, passing it the database folder, and where I want to create the item in relation to that folder. I set the Title to be the same as the title of the source item. Then I call AppendData. But I have to get the content from somewhere. Luckily I can call item.GetContent. The first parameter is where I want to start from (byte 0), how big the file is (item.size), and whether I want the annotations. Then I tell it that I want to stop when it gets to 100 percent of the file. After all the files are copied over, I call EndStreamData and we are all done.

    Its a pretty cool solution and could be useful if you aren't ready to upgrade to SP4.  Enjoy!

     

    Matt (Trainer Matts Blog / Technovangelist)

  • The Alchemy SDK CoolTools Session in Kuala Lumpur

    A few weeks ago we had our annual International Partner Conference in Kuala Lumpur. At that conference I delivered a session on using the Alchemy SDK in interesting ways. It was mostly a demo session with as few Powerpoint slides as possible. I was planning to wait to describe the session and the tools demonstrated until the Visual Studio projects were published on this site. Unfortunately, the process for uploading the files seems to be taking a long time, so I will just start writing. Hopefully soon they will show up here and I will be sure to let you know when they do. For now, this post will describe each of the tools. Later posts will go into detail on each tool and possibly list the full source code in the blog post. One of my rules for creating the tools was that each had to be less than about 100 lines of code. So here we go.

    First since we are dealing with Alchemy we need to have a database. Out of the box, creating a database and adding it to server control means opening Alchemy Administrator, creating the database, closing Administrator, opening the server console, adding the database to the server, closing the server console, and opening Administrator. Wow! That's a lot of steps! So my first app was a simple tool to create a new database and add it to server control in one step.

    But this doesn't actually do everything. It didn't add the database to the alchemy.ini file so it won't appear in Administrator next time you launch it. But that gave me an excuse for the next tool. So sometimes I use a development machine that's actually a virtual machine. The VM has the Alchemy server, and a bunch of other services. I already have Visual Studio on my host machine and I don't want to install it again in the VM, so I use the host as my client, and the VM as my server. That works great! But when I want to add a database that is under server control, finding the database can be tough. Its not on My Computer and my Domain is the Captaris domain. While I may think of myself as important, IT sees me as another loser user and doesn't want me adding servers to the domain willy nilly. So how do I find the new database on my dev server? Well, my DBChooser makes that easy. I simply specify a server name and I see a list of all databases under server control. The databases that are selected are already in the alchemy.ini. I can unselect and select any of the databases and click the save button. Now the next time I launch Administrator, only the selected databases will appear.

    So now that I have a database, I need to add content to the database. I had a bunch of tif files representing invoices that I wanted to add, so I launched DnDOCR. From a simple form I choose a database from the list. Then I can drag and drop one or many files onto that form. Alchemy will add all the files to the root of that database, OCR each of them, and then build the database, all in one step.

    Sometimes I want to add other files that I found in the file explorer in Windows. So I selected a bunch of them, clicked the right mouse button, and chose Add to Alchemy. This is a nice tool, but we quickly run into a limitation of this implementation. I am consuming an administrator license for every file I selected. It will release those licenses when done, but adding 100 files at a time means having 100 admin licenses...not likely. So I have another version of the application that uses web services. The benefit here is that no licenses are used. Yeah, no licenses, zip, nil, nada. The downside is that consuming our webservices are a royal pain in the...well, you know, and they aren't all that speedy. But 0 licenses!!!!

    OK, so now we have some documents in our database. Now I want to search for that content. I used the SimpleSearch I talked about in a previous blog post. There were a few interesting things about that tool. First, there were no buttons or icons, just type what you want. Also, I only consume a Search license, not an Administrator license.

    Searching is nice but there are times I want to do a search and retrieve all the results to a particular folder. So I made a simply Retrieve command line program that just takes two parameters: the search query and a location to retrieve the files to. You can even leave out the location and it will retrieve the search results to the current directory. The search is performed on all databases in your Alchemy.ini file.

    From there I went to workflow integration. Some of our customers want a simple workflow integrated in with Alchemy. Captaris Workflow is great, but it could be beyond the budget and all they want is a simple one step approval. So the next app "workflow enables" any database and adds a few extra buttons to the Administrator interface. When I drag and drop a file to the database for the first time, an unprocessed folder is created and the file is moved into that folder. If I click on the document in that folder and click the Approved button, the document is moved back to the root. Later on I can click on the Request Review button. At that point, the document is moved to the Review Requested folder. After reviewing, someone can then Approve to move it back to the root.

    Next I dealt with security. Imagine the problem: I have a database and 1000 users. I want each user to have their own folder that no one else can access. When they enter the database they only see their personal folder. How would you set that up? Well, you would probably hire a student to come in as an intern (meaning they work hard and get paid nothing), teach them how to do all the steps, then have them slave away for the next few weeks to add each user individually to the database as a group, add a folder, add a role to the server console, add their username to the role, add the database to the role, assign their group to the role, assign certain licenses to the role, etc. Or you could run my command which looks in the Active Directory for the group AlchemyUsers and performs all of those steps for all members of that group all at once. And it takes seconds rather than weeks.

    For anyone new to Alchemy development, creating all of these applications would guarantee at least one thing: a complete monopoly on all licenses...well, at least the Administrator ones. Each of my applications only use the license that it really needs. And to confirm that, I have a little AJAX enabled web site that checks every few seconds to see how many licenses of each type are available. I am using the standard Alchemy Object Library for this and creating objects then destroying them every 5 seconds.

    So that's 9 cool little utilities. Again, all of them were less than 100 lines (or not that much more than 100). And I will go into detail about each of them in the coming days. I hope you will find it interesting...

  • Alchemy Bulk Loader

    Yesterday I finished up another Alchemy SDK class. One of the requests that came up was for a bulk loader utility. The customer had exported the data from some other file management tool as a series of files and an XML file describing it all and wanted to bring it in to Alchemy. So I tackled the problem on the last evening before the end of the class and ended up with a pretty good solution. But I didn't actually have a source file beyond what the customer described in class, so I built a tool to come up with bogus data as well.

    First lets look at the XML Export File. Its pretty simple stuff.

    <Documents>
      <Document>
        <DocumentName>ry7yGR9GDTPD</DocumentName>
        <ExportedFilePath>c:\test\9.tif</ExportedFilePath>
        <DatabasePath>Amsterdam</DatabasePath>
        <FileDate>04/02/00</FileDate>
        <LoanDate>08/02/03</LoanDate>
        <LoanAmount>14811.90</LoanAmount>
      </Document>
    <Documents>

    Each of the elements other than the file's location needed to be turned into fields in the Alchemy Database.

    The Bulk Loader utility simply reads that in as an XMLDocument, creates the fields if necessary, then loops through all the Document elements adding the file and setting the fields. Adding 5000 single page tiffs took less than five minutes.

    class BulkImport
    {
        static string XmlFilePath = "";
        static string DatabasePath = "";
     
        static Alchemy.Application auApp = new Alchemy.Application();
        static Alchemy.Database auDB;
     
        static void Main(string[] args)
        {
            if (GetParameters(args))
            {
                //auApp.LoadOptionsFile("");
     
                try
                {
                    auDB = auApp.Databases.Add(DatabasePath);
                }
                catch (System.Runtime.InteropServices.COMException ex)
                {
                    Console.WriteLine("There was a problem adding the database." + ex.ToString());
                    return;
                }
     
                CreateFields();
     
                XmlDocument ImportedFiles = new XmlDocument();
                ImportedFiles.Load(XmlFilePath);
                foreach (XmlNode documentNode in ImportedFiles.SelectNodes("/Documents/*"))
                    AddDocumentToDatabase(documentNode);
            }
        }
     
        /// <param name="documentNode">Node in the XML file describing the document to be added</param>
        private static void AddDocumentToDatabase(XmlNode documentNode)
        {
            string documentName = documentNode["DocumentName"].InnerText;
            string sourcePath = documentNode["ExportedFilePath"].InnerText;
            string databasePath = documentNode["DatabasePath"].InnerText;
            DateTime fileDate = Convert.ToDateTime(documentNode["FileDate"].InnerText);
            DateTime loanDate = Convert.ToDateTime(documentNode["LoanDate"].InnerText);
            string loanAmount = documentNode["LoanAmount"].InnerText;
            Alchemy.Item newItem = auDB.Root;
            Alchemy.Item Folder = auDB.GetItemByTitlePath(databasePath, auDB.Root) ?? CreateFolder(databasePath);
            newItem = Folder.CreateItem(Alchemy.AuPosEnum.auPosLastChild, sourcePath);
     
            newItem.set_Field("Document Title", documentName);
            newItem.set_Field("ExportedFilePath", sourcePath);
            newItem.set_Field("FileDate", fileDate);
            newItem.set_Field("LoanDate", loanDate);
            newItem.set_Field("LoanAmount", loanAmount);
        }
     
        /// <param name="FolderName">Name of the folder where the item is being added</param>
        /// <returns>The folder that is created</returns>
        static Alchemy.Item CreateFolder(string FolderName)
        {
            Alchemy.Item Folder = auDB.Root.CreateFolder(Alchemy.AuPosEnum.auPosLastChild);
            Folder.set_Field("Folder Title", FolderName);
            return Folder;
        }
     
        static bool GetParameters(string[] args)
        {
            bool returnValue = false;
            if (args.Length==2)
            {
                XmlFilePath = args[0];
                DatabasePath = args[1];
                returnValue = true;
            }
            else
                Console.WriteLine("GetParameters: The correct syntax for this tool is BulkImport XmlFilePath DatabasePath");
     
            return returnValue;
        }
     
        /// <summary>
        /// If the fields in the database do not already exist, create them.
        /// </summary>
        static void CreateFields()
        {
            string[] XmlFields = { "ExportedFilePath", "FileDate","LoanDate","LoanAmount" };
     
            foreach (string fieldName in XmlFields)
            {
                bool fieldExists = false;
                foreach (Alchemy.Field dbField in auDB.Fields)
                {
                    if (fieldName == dbField.Name)
                        fieldExists = true;
                }
     
                if (!fieldExists)
                {
                    Alchemy.AuDataTypeEnum dtEnum = Alchemy.AuDataTypeEnum.auDataText;
                    if (fieldName.ToLower().Contains("date"))
                        dtEnum = Alchemy.AuDataTypeEnum.auDataDate;
                    auDB.Fields.Add(fieldName, dtEnum);
                }
            }
     
        }
    }

    The only step I don't do is a build, but that would have been easy enough to add as well.

    The Test XML Creation tool was also an easy little app, but potentially very useful for me in the future, so I am posting that here too. Whats interesting about this is the randomness. I come up with random dates, names, and folder names. Nothing too complex, but enough to keep it interesting.

     
    class XMLCreate
    {
        static void Main(string[] args)
        {
            if (args.Length > 0)
            {
                int count = 0;
                count = Convert.ToInt32(args[0]);
     
                XmlDocument xDoc = new XmlDocument();
     
                XmlDeclaration xDeclare = xDoc.CreateXmlDeclaration("1.0", "utf-8", null);
                XmlElement rootNode = xDoc.CreateElement("Documents");
                xDoc.InsertBefore(xDeclare, xDoc.DocumentElement);
                xDoc.AppendChild(rootNode);
                Random rnd = new Random(DateTime.Now.Second);
                for (int i = 0; i < count; i++)
                {                    
                    XmlElement documentNode = CreateDocNode(xDoc);
                    CreateDocName(xDoc, documentNode, rnd);
                    CreateFilePath(xDoc, documentNode, rnd);
                    CreateDatabasePath(xDoc, documentNode, rnd);
                    CreateFileDate(xDoc,documentNode,rnd);
                    CreateLoanDate(xDoc, documentNode, rnd);
                    CreateLoanAmount(xDoc, documentNode, rnd);
                }
     
                xDoc.Save("c:\\testxml.xml");
     
            }
        }
     
        private static void CreateNode(XmlDocument xDoc, XmlElement documentNode, string elementName, string elementText)
        {
            XmlElement NodeName = xDoc.CreateElement(elementName);
            XmlText NodeText = xDoc.CreateTextNode(elementText);
            documentNode.AppendChild(NodeName);
            NodeName.AppendChild(NodeText);
        }
     
        private static void CreateLoanAmount(XmlDocument xDoc, XmlElement documentNode, Random rnd)
        {
            Double randomLoanAmount = rnd.NextDouble() * rnd.Next(1000, 100000);
     
            CreateNode(xDoc, documentNode, "LoanAmount", randomLoanAmount.ToString("#.00"));
        }
     
        private static void CreateLoanDate(XmlDocument xDoc, XmlElement documentNode, Random rnd)
        {
            DateTime randomDate = new DateTime(rnd.Next(1995, 2007), rnd.Next(1, 12), rnd.Next(1, 28));
     
            CreateNode(xDoc, documentNode, "LoanDate", randomDate.ToString("MM/dd/yy"));
        }
     
        private static void CreateFileDate(XmlDocument xDoc, XmlElement documentNode, Random rnd)
        {
            DateTime randomDate = new DateTime(rnd.Next(1995, 2007), rnd.Next(1, 12), rnd.Next(1, 28));
     
            CreateNode(xDoc, documentNode, "FileDate", randomDate.ToString("MM/dd/yy"));            
        }
     
        private static void CreateDatabasePath(XmlDocument xDoc, XmlElement documentNode, Random rnd)
        {
            string[] cityNames = new string[] { "Seattle", "Amsterdam", "Dublin", "Cairo" };
            string randomDatabasePath = cityNames[rnd.Next(1, cityNames.Length)];
     
            CreateNode(xDoc, documentNode, "DatabasePath", randomDatabasePath);
        }
     
        private static void CreateFilePath(XmlDocument xDoc, XmlElement documentNode, Random rnd)
        {
            string randomFilePath = "c:\\test\\" + rnd.Next(1, 10).ToString() + ".tif";
     
            CreateNode(xDoc, documentNode, "ExportedFilePath", randomFilePath);
        }
     
        private static XmlElement CreateDocNode(XmlDocument xDoc)
        {
            XmlElement docNode = xDoc.CreateElement("Document");
            xDoc.DocumentElement.PrependChild(docNode);
     
            return docNode;
        }
     
        private static void CreateDocName(XmlDocument xDoc, XmlElement documentNode, Random rnd)
        {
            char[] availChars = "abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ123456789".ToCharArray();
     
            string randomWord = "";
            char ch;
     
            for (int i = 0; i < rnd.Next(10, 15); i++)
            {
                int randomCharPosition = rnd.Next(1, availChars.Length);
                randomWord += availChars[randomCharPosition];
            }
     
            CreateNode(xDoc, documentNode, "DocumentName", randomWord);
        }
    }
  • Captaris Workflow - Getting the Overdue Event to Fire

    One of the common questions I get from Captaris customers is how to get the overdue event to fire. The reason this question comes up is that some of our method names are downright confusing, and I think we changed something here because it doesn't look the same as I remember it.

    In Captaris Workflow, there are a series of events that fire on any given task: Ready, Execute, Overdue, Failed, Reset, and Complete. Ready fires when the task is ready, but nothing has been done. Execute means a task has started but not necessarily finished. Overdue means the time alloted to complete the task has passed, but its not complete yet. Failed means something went wrong. Reset happens when the task is reset by the owner or administrator. And Complete means the task is complete and moves on to the next. At each one of these events, something can happen...anything you want.

    There are two ways to set when that overdue event fires. By default, the task becomes overdue 1 year after it becomes ready. Usually you will want to change that. The easiest way is to drag the Set Overdue Date custom action on to the Ready event of the task and set it to the relative date it will be overdue. This will be Now plus some amount of time. There is no coding required to do this. But sometimes you will want a bit more flexibility that what this offers. For that, use the SetReminderForOverdueEvent method. It expects a DateTime, so just give it a time for when the overdue event should fire.

    public void secondTask_ready(object data)
    {
        SecondTask.SetReminderForOverdueEvent(DateTime.Now.AddSeconds(30));
    }
  • A Simple Search Application for Alchemy

    One of my students in a Dubai Alchemy SDK class asked about creating an extremely simple search client for Alchemy. Out of the box, Alchemy Search can be fairly easy to use, but there are still a lot of buttons that one could press. And if all you want is a simple search to show all the documents across all your repositories it may be a bit too much. So I created what has to be one of the simplest UIs possible. Since I can't seem to post pictures on this blog, I have to describe it...Imagine a window with 3 controls: top left is a textbox. Below that is a listbox and to the right of both is a empty panel.

    As you type in the search box at the top left, results start showing up in the list below then you just click on a document and you get a preview in the viewer.

    You can continue typing and your search is refined. No need to press enter. So how did I get here? Well, its just a simple Windows form application with a SplitContainer. Whenever the text in the text box changes, it does the search again. One of the benefits of Alchemy is that the search is extremely quick, so this app is amazingly quick.

    When I do the search I have to ensure that the text doesn't end with and or or. If it does, then don't pass it to the search because those are keywords we use. Then I clear the textbox and clear the query. The search itself is easy:

    auQuery.AddFullTextQuery(tbSearch.Text);
    auQuery.SearchGroup(auSGroup);
    
    if (auQuery.Results.Count > 0)
    {
        foreach (Alchemy.Result aResult in auQuery.Results)
        {
            foreach (Alchemy.Item aItem in aResult.Items)
            {
                lbResults.Items.Add(aItem.Title);
            }
        }
    }
    

    This also populates the listbox. Since I wanted to make it as simple as possible, I don't even show database names. Then when I click on an item, I tell the Viewer to ViewItem(). It took all of 20 minutes to write ugly code while exhausted.

  • Introductory Post

    Hello there, My name is Matt Williams and this is my first posting on this blog site. Many of you know me, but some of you don't, especially the US-based customers. I own training for all three products outside of the US. This means that I deliver training for developers, admins, and users for Workflow, Rightfax, and Alchemy in places like Australia, Singapore, Croatia, Norway, and more. I am now based in Vianen in the Netherlands, about 30 miles south of Amsterdam. When delivering those classes, I get some bizarre questions and I plan to answer some of them here for everyone to see.

    I am not new to blogging as I have been doing this for a very long time on technovangelist.com and mattweb.org before that. In fact, I was one of the first 4 or 5 public bloggers at Microsoft (before Scoble got there). My first blog post (same idea, but they weren't called blogs yet) was in 1997 or so and was about something stupid....I plan to have no stupid posts here. I will be moving some of the more relevant Captaris content from there to here and will make technovangelist more focused on personal stuff and general technology.

    I am actually on vacation now in Cambodia (for more about that, visit technovangelist.com) so the posting will be light until next week.

This Blog

Post Calendar

<May 2008>
SuMoTuWeThFrSa
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567

Syndication

  Privacy    Site Terms   Contact Administrator